diff options
126 files changed, 4216 insertions, 1973 deletions
diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 099dbbc2764f..2ca9f2e8cdd6 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1982,6 +1982,7 @@ package android.media { method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public float getRs2Value(); method public int getStreamMinVolumeInt(int); method @NonNull public java.util.Map<java.lang.Integer,java.lang.Boolean> getSurroundFormats(); + method @NonNull public android.media.VolumePolicy getVolumePolicy(); method public boolean hasRegisteredDynamicPolicy(); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public boolean isCsdEnabled(); method @RequiresPermission(anyOf={android.Manifest.permission.MODIFY_AUDIO_ROUTING, android.Manifest.permission.QUERY_AUDIO_STATE, android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED}) public boolean isFullVolumeDevice(); @@ -2073,6 +2074,20 @@ package android.media { method public android.media.PlaybackParams setAudioStretchMode(int); } + public final class VolumePolicy implements android.os.Parcelable { + ctor public VolumePolicy(boolean, boolean, boolean, int); + method public int describeContents(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field public static final int A11Y_MODE_INDEPENDENT_A11Y_VOLUME = 1; // 0x1 + field public static final int A11Y_MODE_MEDIA_A11Y_VOLUME = 0; // 0x0 + field @NonNull public static final android.os.Parcelable.Creator<android.media.VolumePolicy> CREATOR; + field @NonNull public static final android.media.VolumePolicy DEFAULT; + field public final boolean doNotDisturbWhenSilent; + field public final int vibrateToSilentDebounce; + field public final boolean volumeDownToEnterSilent; + field public final boolean volumeUpToExitSilent; + } + public static final class VolumeShaper.Configuration.Builder { method @NonNull public android.media.VolumeShaper.Configuration.Builder setOptionFlags(int); } diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java index aca9bb4d07c1..531537c374ce 100644 --- a/core/java/android/app/TaskInfo.java +++ b/core/java/android/app/TaskInfo.java @@ -30,6 +30,7 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; +import android.net.Uri; import android.os.Build; import android.os.IBinder; import android.os.Parcel; @@ -303,6 +304,19 @@ public class TaskInfo { public boolean isTopActivityStyleFloating; /** + * The URI of the intent that generated the top-most activity opened using a URL. + * @hide + */ + @Nullable + public Uri capturedLink; + + /** + * The time of the last launch of the activity opened using the {@link #capturedLink}. + * @hide + */ + public long capturedLinkTimestamp; + + /** * Encapsulate specific App Compat information. * @hide */ @@ -436,6 +450,8 @@ public class TaskInfo { && Objects.equals(topActivity, that.topActivity) && isTopActivityTransparent == that.isTopActivityTransparent && isTopActivityStyleFloating == that.isTopActivityStyleFloating + && Objects.equals(capturedLink, that.capturedLink) + && capturedLinkTimestamp == that.capturedLinkTimestamp && appCompatTaskInfo.equalsForTaskOrganizer(that.appCompatTaskInfo); } @@ -506,6 +522,8 @@ public class TaskInfo { displayAreaFeatureId = source.readInt(); isTopActivityTransparent = source.readBoolean(); isTopActivityStyleFloating = source.readBoolean(); + capturedLink = source.readTypedObject(Uri.CREATOR); + capturedLinkTimestamp = source.readLong(); appCompatTaskInfo = source.readTypedObject(AppCompatTaskInfo.CREATOR); } @@ -554,6 +572,8 @@ public class TaskInfo { dest.writeInt(displayAreaFeatureId); dest.writeBoolean(isTopActivityTransparent); dest.writeBoolean(isTopActivityStyleFloating); + dest.writeTypedObject(capturedLink, flags); + dest.writeLong(capturedLinkTimestamp); dest.writeTypedObject(appCompatTaskInfo, flags); } @@ -592,6 +612,8 @@ public class TaskInfo { + " displayAreaFeatureId=" + displayAreaFeatureId + " isTopActivityTransparent=" + isTopActivityTransparent + " isTopActivityStyleFloating=" + isTopActivityStyleFloating + + " capturedLink=" + capturedLink + + " capturedLinkTimestamp=" + capturedLinkTimestamp + " appCompatTaskInfo=" + appCompatTaskInfo + "}"; } diff --git a/core/java/android/content/pm/ShortcutServiceInternal.java b/core/java/android/content/pm/ShortcutServiceInternal.java index 55d0bc1deedc..c811a47e5b05 100644 --- a/core/java/android/content/pm/ShortcutServiceInternal.java +++ b/core/java/android/content/pm/ShortcutServiceInternal.java @@ -91,6 +91,9 @@ public abstract class ShortcutServiceInternal { public abstract void addShortcutChangeCallback( @NonNull LauncherApps.ShortcutChangeCallback callback); + public abstract void removeShortcutChangeCallback( + @NonNull LauncherApps.ShortcutChangeCallback callback); + public abstract int getShortcutIconResId(int launcherUserId, @NonNull String callingPackage, @NonNull String packageName, @NonNull String shortcutId, int userId); diff --git a/core/java/android/hardware/OWNERS b/core/java/android/hardware/OWNERS index 51ad1519941b..43d3f5466ccf 100644 --- a/core/java/android/hardware/OWNERS +++ b/core/java/android/hardware/OWNERS @@ -5,7 +5,7 @@ michaelwr@google.com sumir@google.com # Camera -per-file *Camera*=cychen@google.com,epeev@google.com,etalvala@google.com,shuzhenwang@google.com,zhijunhe@google.com,jchowdhary@google.com +per-file *Camera*=file:platform/frameworks/av:/camera/OWNERS # Sensor Privacy per-file *SensorPrivacy* = file:platform/frameworks/native:/libs/sensorprivacy/OWNERS diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index acd0d00f812d..16d9ef270f75 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -61,3 +61,10 @@ flag { description: "Allows system to provide keyboard specific key drawables and shortcuts via config files" bug: "345440920" } + +flag { + namespace: "input_native" + name: "keyboard_a11y_mouse_keys" + description: "Controls if the mouse keys accessibility feature for physical keyboard is available to the user" + bug: "341799888" +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index c954cdb270e8..2562c8e31095 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -12324,6 +12324,18 @@ public final class Settings { "accessibility_force_invert_color_enabled"; /** + * Whether to enable mouse keys for Physical Keyboard accessibility. + * + * If set to true, key presses (of the mouse keys) on + * physical keyboard will control mouse pointer on the display. + * + * @hide + */ + @Readable + public static final String ACCESSIBILITY_MOUSE_KEYS_ENABLED = + "accessibility_mouse_keys_enabled"; + + /** * Whether the Adaptive connectivity option is enabled. * * @hide diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index e54853631d5f..245c0e7c630c 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -122,6 +122,13 @@ flag { } flag { + name: "enable_caption_compat_inset_force_consumption" + namespace: "lse_desktop_experience" + description: "Enables force-consumption of caption bar insets for immersive apps in freeform" + bug: "316231589" +} + +flag { name: "show_desktop_windowing_dev_option" namespace: "lse_desktop_experience" description: "Whether to show developer option for enabling desktop windowing mode" diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java index 1638699c6175..5c818f178739 100644 --- a/core/java/com/android/internal/jank/Cuj.java +++ b/core/java/com/android/internal/jank/Cuj.java @@ -175,9 +175,17 @@ public class Cuj { /** Track launching a dialog from a status bar chip. */ public static final int CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP = 111; + /** Track Launcher Keyboard Quick Switch View opening animation */ + public static final int CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN = 112; + + /** Track Launcher Keyboard Quick Switch View closing animation */ + public static final int CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE = 113; + + /** Track launching an app through the Launcher Keyboard Quick Switch View */ + public static final int CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH = 114; // When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE. - @VisibleForTesting static final int LAST_CUJ = CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP; + @VisibleForTesting static final int LAST_CUJ = CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH; /** @hide */ @IntDef({ @@ -280,7 +288,10 @@ public class Cuj { CUJ_DESKTOP_MODE_EXIT_MODE, CUJ_DESKTOP_MODE_MINIMIZE_WINDOW, CUJ_DESKTOP_MODE_DRAG_WINDOW, - CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP + CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, + CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN, + CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE, + CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH }) @Retention(RetentionPolicy.SOURCE) public @interface CujType {} @@ -394,6 +405,9 @@ public class Cuj { CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_MINIMIZE_WINDOW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_MINIMIZE_WINDOW; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_DRAG_WINDOW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_DRAG_WINDOW; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH; } private Cuj() { @@ -612,6 +626,12 @@ public class Cuj { return "DESKTOP_MODE_DRAG_WINDOW"; case CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP: return "STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP"; + case CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN: + return "CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN"; + case CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE: + return "CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE"; + case CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH: + return "CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH"; } return "UNKNOWN"; } diff --git a/core/res/res/xml/power_profile.xml b/core/res/res/xml/power_profile.xml index fc63657f04d0..f67ad3f5575e 100644 --- a/core/res/res/xml/power_profile.xml +++ b/core/res/res/xml/power_profile.xml @@ -33,14 +33,14 @@ There must be one of these for each display, labeled: ambient.on.display0, ambient.on.display1, etc... - Each display suffix number should match it's ordinal in its display device config. + Each display suffix number should match its ordinal in its display device config. --> <item name="ambient.on.display0">0.1</item> <!-- ~100mA --> <!-- Average battery current draw of display0 while on without backlight. There must be one of these for each display, labeled: screen.on.display0, screen.on.display1, etc... - Each display suffix number should match it's ordinal in its display device config. + Each display suffix number should match its ordinal in its display device config. --> <item name="screen.on.display0">0.1</item> <!-- ~100mA --> <!-- Average battery current draw of the backlight at full brightness. @@ -50,7 +50,7 @@ There must be one of these for each display, labeled: screen.full.display0, screen.full.display1, etc... - Each display suffix number should match it's ordinal in its display device config. + Each display suffix number should match its ordinal in its display device config. --> <item name="screen.full.display0">0.1</item> <!-- ~100mA --> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_open_in_browser.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_open_in_browser.xml new file mode 100644 index 000000000000..7d912a24c443 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_open_in_browser.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="960" android:viewportHeight="960" android:tint="?attr/colorControlNormal"> + <path android:fillColor="@android:color/black" android:pathData="M160,880Q127,880 103.5,856.5Q80,833 80,800L80,440Q80,407 103.5,383.5Q127,360 160,360L240,360L240,160Q240,127 263.5,103.5Q287,80 320,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,520Q880,553 856.5,576.5Q833,600 800,600L720,600L720,800Q720,833 696.5,856.5Q673,880 640,880L160,880ZM160,800L640,800Q640,800 640,800Q640,800 640,800L640,520L160,520L160,800Q160,800 160,800Q160,800 160,800ZM720,520L800,520Q800,520 800,520Q800,520 800,520L800,240L320,240L320,360L640,360Q673,360 696.5,383.5Q720,407 720,440L720,520Z"/> +</vector> 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 d5724cc6a420..419d5c0af1a4 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 @@ -135,5 +135,24 @@ android:drawableTint="?androidprv:attr/materialColorOnSurface" style="@style/DesktopModeHandleMenuActionButton"/> </LinearLayout> + + <LinearLayout + android:id="@+id/open_in_browser_pill" + android:layout_width="match_parent" + android:layout_height="@dimen/desktop_mode_handle_menu_open_in_browser_pill_height" + android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin" + android:layout_marginStart="1dp" + android:orientation="vertical" + android:elevation="1dp" + android:background="@drawable/desktop_mode_decor_handle_menu_background"> + + <Button + android:id="@+id/open_in_browser_button" + android:contentDescription="@string/open_in_browser_text" + android:text="@string/open_in_browser_text" + android:drawableStart="@drawable/desktop_mode_ic_handle_menu_open_in_browser" + android:drawableTint="?androidprv:attr/materialColorOnSurface" + style="@style/DesktopModeHandleMenuActionButton"/> + </LinearLayout> </LinearLayout> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 595d34664cfa..d143263b69a5 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -507,8 +507,11 @@ <!-- The height of the handle menu's "More Actions" pill in desktop mode. --> <dimen name="desktop_mode_handle_menu_more_actions_pill_height">52dp</dimen> + <!-- The height of the handle menu's "Open in browser" pill in desktop mode. --> + <dimen name="desktop_mode_handle_menu_open_in_browser_pill_height">52dp</dimen> + <!-- The height of the handle menu in desktop mode. --> - <dimen name="desktop_mode_handle_menu_height">328dp</dimen> + <dimen name="desktop_mode_handle_menu_height">380dp</dimen> <!-- The top margin of the handle menu in desktop mode. --> <dimen name="desktop_mode_handle_menu_margin_top">4dp</dimen> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 47846746b205..4e7cfb638a12 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -280,6 +280,8 @@ <string name="select_text">Select</string> <!-- Accessibility text for the handle menu screenshot button [CHAR LIMIT=NONE] --> <string name="screenshot_text">Screenshot</string> + <!-- Accessibility text for the handle menu open in browser button [CHAR LIMIT=NONE] --> + <string name="open_in_browser_text">Open in browser</string> <!-- Accessibility text for the handle menu close button [CHAR LIMIT=NONE] --> <string name="close_text">Close</string> <!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] --> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 1be33e5d2d95..faf6a627febe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.view.InputDevice.SOURCE_TOUCHSCREEN; import static android.view.MotionEvent.ACTION_CANCEL; import static android.view.MotionEvent.ACTION_HOVER_ENTER; @@ -41,12 +42,17 @@ import android.annotation.NonNull; import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityTaskManager; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region; import android.hardware.input.InputManager; +import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; @@ -410,6 +416,26 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { decoration.closeMaximizeMenu(); } + private void onOpenInBrowser(@NonNull DesktopModeWindowDecoration decor, @NonNull Uri uri) { + openInBrowser(uri); + decor.closeHandleMenu(); + decor.closeMaximizeMenu(); + } + + private void openInBrowser(Uri uri) { + final Intent intent = new Intent(Intent.ACTION_VIEW, uri) + .setComponent(getDefaultBrowser()) + .addFlags(FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(intent); + } + + private ComponentName getDefaultBrowser() { + final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://")); + final ResolveInfo info = mContext.getPackageManager() + .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); + return info.getComponentInfo().getComponentName(); + } + private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener, View.OnGenericMotionListener, DragDetector.MotionEventHandler { @@ -489,6 +515,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } else if (id == R.id.split_screen_button) { decoration.closeHandleMenu(); mDesktopTasksController.requestSplit(decoration.mTaskInfo); + } else if (id == R.id.open_in_browser_button) { + // TODO(b/346441962): let the decoration handle the click gesture and only call back + // to the ViewModel via #setOpenInBrowserClickListener + decoration.onOpenInBrowserClick(); } else if (id == R.id.collapse_menu_button) { decoration.closeHandleMenu(); } else if (id == R.id.maximize_window) { @@ -1091,6 +1121,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { windowDecoration.setOnRightSnapClickListener((taskId, tag) -> { onSnapResize(taskId, false /* isLeft */); }); + windowDecoration.setOpenInBrowserClickListener(this::onOpenInBrowser); windowDecoration.setCaptionListeners( touchEventListener, touchEventListener, touchEventListener, touchEventListener); windowDecoration.setExclusionRegionListener(mExclusionRegionListener); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index b62194ca6239..5ffd883a7ceb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -44,6 +44,7 @@ import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.Drawable; +import android.net.Uri; import android.os.Handler; import android.os.Trace; import android.util.Log; @@ -88,6 +89,7 @@ import java.util.function.Supplier; */ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> { private static final String TAG = "DesktopModeWindowDecoration"; + private static final int CAPTURED_LINK_TIMEOUT_MS = 7000; @VisibleForTesting static final long CLOSE_MAXIMIZE_MENU_DELAY_MS = 150L; @@ -124,6 +126,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private Bitmap mResizeVeilBitmap; private CharSequence mAppName; + private CapturedLink mCapturedLink; + private OpenInBrowserClickListener mOpenInBrowserClickListener; private ExclusionRegionListener mExclusionRegionListener; @@ -138,6 +142,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // being hovered. There's a small delay after stopping the hover, to allow a quick reentry // to cancel the close. private final Runnable mCloseMaximizeWindowRunnable = this::closeMaximizeMenu; + private final Runnable mCapturedLinkExpiredRunnable = this::onCapturedLinkExpired; DesktopModeWindowDecoration( Context context, @@ -153,8 +158,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin handler, choreographer, syncQueue, rootTaskDisplayAreaOrganizer, SurfaceControl.Builder::new, SurfaceControl.Transaction::new, WindowContainerTransaction::new, SurfaceControl::new, - new SurfaceControlViewHostFactory() {}, - DefaultMaximizeMenuFactory.INSTANCE); + new SurfaceControlViewHostFactory() {}, DefaultMaximizeMenuFactory.INSTANCE); } DesktopModeWindowDecoration( @@ -232,6 +236,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mDragDetector.setTouchSlop(ViewConfiguration.get(mContext).getScaledTouchSlop()); } + void setOpenInBrowserClickListener(OpenInBrowserClickListener listener) { + mOpenInBrowserClickListener = listener; + } + @Override void relayout(ActivityManager.RunningTaskInfo taskInfo) { final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); @@ -323,6 +331,11 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) { Trace.beginSection("DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces"); + + if (Flags.enableDesktopWindowingAppToWeb()) { + setCapturedLink(taskInfo.capturedLink, taskInfo.capturedLinkTimestamp); + } + if (isHandleMenuActive()) { mHandleMenu.relayout(startT); } @@ -367,6 +380,28 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces } + private void setCapturedLink(Uri capturedLink, long timeStamp) { + if (capturedLink == null + || (mCapturedLink != null && mCapturedLink.mTimeStamp == timeStamp)) { + return; + } + mCapturedLink = new CapturedLink(capturedLink, timeStamp); + mHandler.postDelayed(mCapturedLinkExpiredRunnable, CAPTURED_LINK_TIMEOUT_MS); + } + + private void onCapturedLinkExpired() { + mHandler.removeCallbacks(mCapturedLinkExpiredRunnable); + if (mCapturedLink != null) { + mCapturedLink.setExpired(); + } + } + + void onOpenInBrowserClick() { + if (mOpenInBrowserClickListener == null || mCapturedLink == null) return; + mOpenInBrowserClickListener.onClick(this, mCapturedLink.mUri); + onCapturedLinkExpired(); + } + private void updateDragResizeListener(SurfaceControl oldDecorationSurface) { if (!isDragResizable(mTaskInfo)) { if (!mTaskInfo.positionInParent.equals(mPositionInParent)) { @@ -827,11 +862,17 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin .setCaptionHeight(mResult.mCaptionHeight) .setDisplayController(mDisplayController) .setSplitScreenController(splitScreenController) + .setBrowserLinkAvailable(browserLinkAvailable()) .build(); mWindowDecorViewHolder.onHandleMenuOpened(); mHandleMenu.show(); } + @VisibleForTesting + boolean browserLinkAvailable() { + return mCapturedLink != null && !mCapturedLink.mExpired; + } + /** * Close the handle menu window. */ @@ -1121,6 +1162,31 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } } + @VisibleForTesting + static class CapturedLink { + private final long mTimeStamp; + private final Uri mUri; + private boolean mExpired; + + CapturedLink(@NonNull Uri uri, long timeStamp) { + mUri = uri; + mTimeStamp = timeStamp; + mExpired = false; + } + + void setExpired() { + mExpired = true; + } + } + + + /** Listener for the handle menu's "Open in browser" button */ + interface OpenInBrowserClickListener { + + /** Inform the implementing class that the "Open in browser" button has been clicked */ + void onClick(DesktopModeWindowDecoration decoration, Uri uri); + } + interface ExclusionRegionListener { /** Inform the implementing class of this task's change in region resize handles */ void onExclusionRegionChanged(int taskId, Region region); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java index df0836c1121d..7e44f32bcbeb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java @@ -42,6 +42,7 @@ import android.graphics.Rect; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; +import android.widget.Button; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; @@ -81,6 +82,7 @@ class HandleMenu { // those as well. final Point mGlobalMenuPosition = new Point(); private final boolean mShouldShowWindowingPill; + private final boolean mShouldShowBrowserPill; private final Bitmap mAppIconBitmap; private final CharSequence mAppName; private final View.OnClickListener mOnClickListener; @@ -101,7 +103,7 @@ class HandleMenu { View.OnClickListener onClickListener, View.OnTouchListener onTouchListener, Bitmap appIcon, CharSequence appName, DisplayController displayController, SplitScreenController splitScreenController, boolean shouldShowWindowingPill, - int captionHeight) { + boolean shouldShowBrowserPill, int captionHeight) { mParentDecor = parentDecor; mContext = mParentDecor.mDecorWindowContext; mTaskInfo = mParentDecor.mTaskInfo; @@ -113,6 +115,7 @@ class HandleMenu { mAppIconBitmap = appIcon; mAppName = appName; mShouldShowWindowingPill = shouldShowWindowingPill; + mShouldShowBrowserPill = shouldShowBrowserPill; mCaptionHeight = captionHeight; loadHandleMenuDimensions(); updateHandleMenuPillPositions(); @@ -170,6 +173,7 @@ class HandleMenu { setupWindowingPill(handleMenu); } setupMoreActionsPill(handleMenu); + setupOpenInBrowserPill(handleMenu); } /** @@ -228,6 +232,15 @@ class HandleMenu { } } + private void setupOpenInBrowserPill(View handleMenu) { + if (!mShouldShowBrowserPill) { + handleMenu.findViewById(R.id.open_in_browser_pill).setVisibility(View.GONE); + return; + } + final Button browserButton = handleMenu.findViewById(R.id.open_in_browser_button); + browserButton.setOnClickListener(mOnClickListener); + } + /** * Returns array of windowing icon color based on current UI theme. First element of the * array is for inactive icons and the second is for active icons. @@ -423,6 +436,10 @@ class HandleMenu { menuHeight -= loadDimensionPixelSize(resources, R.dimen.desktop_mode_handle_menu_more_actions_pill_height); } + if (!mShouldShowBrowserPill) { + menuHeight -= loadDimensionPixelSize(resources, + R.dimen.desktop_mode_handle_menu_open_in_browser_pill_height); + } return menuHeight; } @@ -457,6 +474,7 @@ class HandleMenu { private int mCaptionHeight; private DisplayController mDisplayController; private SplitScreenController mSplitScreenController; + private boolean mShowBrowserPill; Builder(@NonNull DesktopModeWindowDecoration parent) { mParent = parent; @@ -507,10 +525,15 @@ class HandleMenu { return this; } + Builder setBrowserLinkAvailable(Boolean showBrowserPill) { + mShowBrowserPill = showBrowserPill; + return this; + } + HandleMenu build() { return new HandleMenu(mParent, mLayoutId, mOnClickListener, mOnTouchListener, mAppIcon, mName, mDisplayController, mSplitScreenController, - mShowWindowingPill, mCaptionHeight); + mShowWindowingPill, mShowBrowserPill, mCaptionHeight); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt index 8c5d4a2c2ffb..25a829b44448 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt @@ -26,6 +26,7 @@ import android.view.View.SCALE_Y import android.view.View.TRANSLATION_Y import android.view.View.TRANSLATION_Z import android.view.ViewGroup +import android.widget.Button import androidx.core.animation.doOnEnd import androidx.core.view.children import com.android.wm.shell.R @@ -72,6 +73,7 @@ class HandleMenuAnimator( private val appInfoPill: ViewGroup = handleMenu.requireViewById(R.id.app_info_pill) private val windowingPill: ViewGroup = handleMenu.requireViewById(R.id.windowing_pill) private val moreActionsPill: ViewGroup = handleMenu.requireViewById(R.id.more_actions_pill) + private val openInBrowserPill: ViewGroup = handleMenu.requireViewById(R.id.open_in_browser_pill) /** Animates the opening of the handle menu. */ fun animateOpen() { @@ -80,6 +82,7 @@ class HandleMenuAnimator( animateAppInfoPillOpen() animateWindowingPillOpen() animateMoreActionsPillOpen() + animateOpenInBrowserPill() runAnimations() } @@ -94,6 +97,7 @@ class HandleMenuAnimator( animateAppInfoPillOpen() animateWindowingPillOpen() animateMoreActionsPillOpen() + animateOpenInBrowserPill() runAnimations() } @@ -109,6 +113,7 @@ class HandleMenuAnimator( animateAppInfoPillFadeOut() windowingPillClose() moreActionsPillClose() + openInBrowserPillClose() runAnimations(after) } @@ -125,6 +130,7 @@ class HandleMenuAnimator( animateAppInfoPillFadeOut() windowingPillClose() moreActionsPillClose() + openInBrowserPillClose() runAnimations(after) } @@ -137,6 +143,7 @@ class HandleMenuAnimator( appInfoPill.children.forEach { it.alpha = 0f } windowingPill.alpha = 0f moreActionsPill.alpha = 0f + openInBrowserPill.alpha = 0f // Setup pivots. handleMenu.pivotX = menuWidth / 2f @@ -147,6 +154,9 @@ class HandleMenuAnimator( moreActionsPill.pivotX = menuWidth / 2f moreActionsPill.pivotY = appInfoPill.measuredHeight.toFloat() + + openInBrowserPill.pivotX = menuWidth / 2f + openInBrowserPill.pivotY = appInfoPill.measuredHeight.toFloat() } private fun animateAppInfoPillOpen() { @@ -268,12 +278,50 @@ class HandleMenuAnimator( // More Actions Content Opacity Animation moreActionsPill.children.forEach { animators += - ObjectAnimator.ofFloat(it, ALPHA, 1f).apply { + ObjectAnimator.ofFloat(it, ALPHA, 1f).apply { + startDelay = BODY_ALPHA_OPEN_DELAY + duration = BODY_CONTENT_ALPHA_OPEN_DURATION + interpolator = Interpolators.FAST_OUT_SLOW_IN + } + } + } + + private fun animateOpenInBrowserPill() { + // Open in Browser X & Y Scaling Animation + animators += + ObjectAnimator.ofFloat(openInBrowserPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply { + startDelay = BODY_SCALE_OPEN_DELAY + duration = BODY_SCALE_OPEN_DURATION + } + + animators += + ObjectAnimator.ofFloat(openInBrowserPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply { + startDelay = BODY_SCALE_OPEN_DELAY + duration = BODY_SCALE_OPEN_DURATION + } + + // Open in Browser Opacity Animation + animators += + ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 1f).apply { + startDelay = BODY_ALPHA_OPEN_DELAY + duration = BODY_ALPHA_OPEN_DURATION + } + + // Open in Browser Elevation Animation + animators += + ObjectAnimator.ofFloat(openInBrowserPill, TRANSLATION_Z, 1f).apply { + startDelay = ELEVATION_OPEN_DELAY + duration = BODY_ELEVATION_OPEN_DURATION + } + + // Open in Browser Button Opacity Animation + val button = openInBrowserPill.requireViewById<Button>(R.id.open_in_browser_button) + animators += + ObjectAnimator.ofFloat(button, ALPHA, 1f).apply { startDelay = BODY_ALPHA_OPEN_DELAY duration = BODY_CONTENT_ALPHA_OPEN_DURATION interpolator = Interpolators.FAST_OUT_SLOW_IN } - } } private fun appInfoPillCollapse() { @@ -379,6 +427,37 @@ class HandleMenuAnimator( } } + private fun openInBrowserPillClose() { + // Open in Browser X & Y Scaling Animation + animators += + ObjectAnimator.ofFloat(openInBrowserPill, SCALE_X, HALF_INITIAL_SCALE).apply { + duration = BODY_CLOSE_DURATION + } + + animators += + ObjectAnimator.ofFloat(openInBrowserPill, SCALE_Y, HALF_INITIAL_SCALE).apply { + duration = BODY_CLOSE_DURATION + } + + // Open in Browser Opacity Animation + animators += + ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 0f).apply { + duration = BODY_CLOSE_DURATION + } + + animators += + ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 0f).apply { + duration = BODY_CLOSE_DURATION + } + + // Upward Open in Browser y-translation Animation + val yStart: Float = -captionHeight / 2 + animators += + ObjectAnimator.ofFloat(openInBrowserPill, TRANSLATION_Y, yStart).apply { + duration = BODY_CLOSE_DURATION + } + } + /** * Runs the list of hide animators concurrently. * diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index b355137a9077..d8606093ac5c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -31,6 +31,7 @@ import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; @@ -50,6 +51,7 @@ import android.content.pm.PackageManager; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.PointF; +import android.net.Uri; import android.os.Handler; import android.os.SystemProperties; import android.platform.test.annotations.DisableFlags; @@ -118,6 +120,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private static final String USE_ROUNDED_CORNERS_SYSPROP_KEY = "persist.wm.debug.desktop_use_rounded_corners"; + private static final Uri TEST_URI = Uri.parse("www.google.com"); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); @Mock @@ -150,6 +154,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private PackageManager mMockPackageManager; @Mock private Handler mMockHandler; + @Mock + private DesktopModeWindowDecoration.OpenInBrowserClickListener mMockOpenInBrowserClickListener; @Captor private ArgumentCaptor<Function1<Boolean, Unit>> mOnMaxMenuHoverChangeListener; @Captor @@ -555,6 +561,65 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { verify(mMockHandler).removeCallbacks(any()); } + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) + public void capturedLink_postsOnCapturedLinkExpiredRunnable() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); + final DesktopModeWindowDecoration decor = createWindowDecoration(taskInfo); + + decor.relayout(taskInfo); + // Assert captured link is set + assertTrue(decor.browserLinkAvailable()); + // Asset runnable posted to set captured link to expired + verify(mMockHandler).postDelayed(runnableArgument.capture(), anyLong()); + runnableArgument.getValue().run(); + assertFalse(decor.browserLinkAvailable()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) + public void capturedLink_capturedLinkNotResetToSameLink() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final DesktopModeWindowDecoration decor = createWindowDecoration(taskInfo); + final ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); + + // Set captured link and run on captured link expired runnable + decor.relayout(taskInfo); + verify(mMockHandler).postDelayed(runnableArgument.capture(), anyLong()); + runnableArgument.getValue().run(); + + decor.relayout(taskInfo); + // Assert captured link not set to same value twice + assertFalse(decor.browserLinkAvailable()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) + public void capturedLink_capturedLinkExpiresAfterClick() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final DesktopModeWindowDecoration decor = createWindowDecoration(taskInfo); + + decor.relayout(taskInfo); + // Assert captured link is set + assertTrue(decor.browserLinkAvailable()); + decor.onOpenInBrowserClick(); + //Assert Captured link expires after button is clicked + assertFalse(decor.browserLinkAvailable()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) + public void capturedLink_openInBrowserListenerCalledOnClick() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final DesktopModeWindowDecoration decor = createWindowDecoration(taskInfo); + + decor.relayout(taskInfo); + decor.onOpenInBrowserClick(); + + verify(mMockOpenInBrowserClickListener).onClick(any(), any()); + } + private void createMaximizeMenu(DesktopModeWindowDecoration decoration, MaximizeMenu menu) { final OnTaskActionClickListener l = (taskId, tag) -> {}; decoration.setOnMaximizeOrRestoreClickListener(l); @@ -595,11 +660,11 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { mMockHandler, mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer, SurfaceControl.Builder::new, mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new, - mMockSurfaceControlViewHostFactory, - maximizeMenuFactory); + mMockSurfaceControlViewHostFactory, maximizeMenuFactory); windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener, mMockTouchEventListener, mMockTouchEventListener); windowDecor.setExclusionRegionListener(mMockExclusionRegionListener); + windowDecor.setOpenInBrowserClickListener(mMockOpenInBrowserClickListener); return windowDecor; } @@ -615,6 +680,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { "DesktopModeWindowDecorationTests"); taskInfo.baseActivity = new ComponentName("com.android.wm.shell.windowdecor", "DesktopModeWindowDecorationTests"); + taskInfo.capturedLink = TEST_URI; + taskInfo.capturedLinkTimestamp = System.currentTimeMillis(); return taskInfo; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt index 5582e0f46321..0c50ab6b5008 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt @@ -196,9 +196,9 @@ class HandleMenuTest : ShellTestCase() { R.layout.desktop_mode_app_header } val handleMenu = HandleMenu(mockDesktopWindowDecoration, layoutId, - onClickListener, onTouchListener, appIcon, appName, displayController, - splitScreenController, true /* shouldShowWindowingPill */, - 50 /* captionHeight */ ) + onClickListener, onTouchListener, appIcon, appName, displayController, + splitScreenController, true /* shouldShowWindowingPill */, + true /* shouldShowBrowserPill */, 50 /* captionHeight */) handleMenu.show() return handleMenu } diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 124f1f0ddd43..25c767a88b17 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -7435,6 +7435,21 @@ public class AudioManager { } /** + * @hide + * Queries the volume policy + * @return the volume policy currently in use + */ + @TestApi + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. + public @NonNull VolumePolicy getVolumePolicy() { + try { + return getService().getVolumePolicy(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Set Hdmi Cec system audio mode. * * @param on whether to be on system audio mode diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 08cc126fb1a8..d42b25681e11 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -386,6 +386,8 @@ interface IAudioService { void setVolumePolicy(in VolumePolicy policy); + VolumePolicy getVolumePolicy(); + boolean hasRegisteredDynamicPolicy(); void registerRecordingCallback(in IRecordingConfigDispatcher rcdb); diff --git a/media/java/android/media/VolumePolicy.java b/media/java/android/media/VolumePolicy.java index b193b70bac2c..96de72dfd10d 100644 --- a/media/java/android/media/VolumePolicy.java +++ b/media/java/android/media/VolumePolicy.java @@ -16,39 +16,53 @@ package android.media; +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.annotation.TestApi; import android.os.Parcel; import android.os.Parcelable; import java.util.Objects; /** @hide */ +@TestApi +@SuppressLint("UnflaggedApi") // @TestApi without associated feature. public final class VolumePolicy implements Parcelable { + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. + @NonNull public static final VolumePolicy DEFAULT = new VolumePolicy(false, false, false, 400); /** * Accessibility volume policy where the STREAM_MUSIC volume (i.e. media volume) affects * the STREAM_ACCESSIBILITY volume, and vice-versa. */ + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. public static final int A11Y_MODE_MEDIA_A11Y_VOLUME = 0; /** * Accessibility volume policy where the STREAM_ACCESSIBILITY volume is independent from * any other volume. */ + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. public static final int A11Y_MODE_INDEPENDENT_A11Y_VOLUME = 1; /** Allow volume adjustments lower from vibrate to enter ringer mode = silent */ + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. public final boolean volumeDownToEnterSilent; /** Allow volume adjustments higher to exit ringer mode = silent */ + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. public final boolean volumeUpToExitSilent; /** Automatically enter do not disturb when ringer mode = silent */ + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. public final boolean doNotDisturbWhenSilent; /** Only allow volume adjustment from vibrate to silent after this number of milliseconds since an adjustment from normal to vibrate. */ + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. public final int vibrateToSilentDebounce; + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. public VolumePolicy(boolean volumeDownToEnterSilent, boolean volumeUpToExitSilent, boolean doNotDisturbWhenSilent, int vibrateToSilentDebounce) { this.volumeDownToEnterSilent = volumeDownToEnterSilent; @@ -82,19 +96,22 @@ public final class VolumePolicy implements Parcelable { && other.vibrateToSilentDebounce == vibrateToSilentDebounce; } + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. @Override public int describeContents() { return 0; } + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. @Override - public void writeToParcel(Parcel dest, int flags) { + public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(volumeDownToEnterSilent ? 1 : 0); dest.writeInt(volumeUpToExitSilent ? 1 : 0); dest.writeInt(doNotDisturbWhenSilent ? 1 : 0); dest.writeInt(vibrateToSilentDebounce); } + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. public static final @android.annotation.NonNull Parcelable.Creator<VolumePolicy> CREATOR = new Parcelable.Creator<VolumePolicy>() { @Override diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java index 66ab81bf02b1..e00533422072 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java @@ -554,11 +554,18 @@ public class CompanionAssociationActivity extends FragmentActivity implements mSelectedDevice = requireNonNull(selectedDevice); Slog.d(TAG, "onDeviceClicked(): " + mSelectedDevice.toShortString()); - + // The permission consent dialog should not be displayed if it's a isSkipPrompt(true) + // AssociationRequest or when there is no device profile available + // for the multiple devices dialog. + // See AssociationRequestsProcessor#mayAssociateWithoutPrompt. + final String deviceProfile = mRequest.getDeviceProfile(); + if (deviceProfile == null || mRequest.isSkipPrompt()) { + onUserSelectedDevice(mSelectedDevice); + return; + } + // The permission consent dialog should be displayed for the multiple device + // dialog if a device profile exists. updateSingleDeviceUi(); - - if (mRequest.isSkipPrompt()) return; - mSummary.setVisibility(View.VISIBLE); mButtonAllow.setVisibility(View.VISIBLE); mButtonNotAllow.setVisibility(View.VISIBLE); @@ -588,9 +595,6 @@ public class CompanionAssociationActivity extends FragmentActivity implements if (deviceProfile == null && mRequest.isSingleDevice()) { summary = getHtmlFromResources(this, summaryResourceId, remoteDeviceName); mConstraintList.setVisibility(View.GONE); - } else if (deviceProfile == null) { - onUserSelectedDevice(mSelectedDevice); - return; } else { summary = getHtmlFromResources( this, summaryResourceId, getString(R.string.device_type)); diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index d4a4703d5caf..5f236516785d 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -228,6 +228,7 @@ public class SecureSettings { Settings.Secure.ACCESSIBILITY_MAGNIFICATION_ALWAYS_ON_ENABLED, Settings.Secure.ACCESSIBILITY_MAGNIFICATION_JOYSTICK_ENABLED, Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED, + Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ENABLED, Settings.Secure.ACCESSIBILITY_PINCH_TO_ZOOM_ANYWHERE_ENABLED, Settings.Secure.ACCESSIBILITY_SINGLE_FINGER_PANNING_ENABLED, Settings.Secure.ODI_CAPTIONS_VOLUME_UI_ENABLED, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 6df1c45bd2ac..c8da8afc25c3 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -439,6 +439,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.ON_DEVICE_INFERENCE_UNBIND_TIMEOUT_MS, ANY_LONG_VALIDATOR); VALIDATORS.put(Secure.ON_DEVICE_INTELLIGENCE_UNBIND_TIMEOUT_MS, ANY_LONG_VALIDATOR); VALIDATORS.put(Secure.ON_DEVICE_INTELLIGENCE_IDLE_TIMEOUT_MS, NONE_NEGATIVE_LONG_VALIDATOR); + VALIDATORS.put(Secure.ACCESSIBILITY_MOUSE_KEYS_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.MANDATORY_BIOMETRICS, new InclusiveIntegerRangeValidator(0, 1)); } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java index e77cf2fa6543..2227943c0cc0 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java @@ -189,22 +189,13 @@ public final class DeviceConfigService extends Binder { public static HashMap<String, String> getAllFlags(IContentProvider provider) { HashMap<String, String> allFlags = new HashMap<String, String>(); - try { - Bundle args = new Bundle(); - args.putInt(Settings.CALL_METHOD_USER_KEY, - ActivityManager.getService().getCurrentUser().id); - Bundle b = provider.call(new AttributionSource(Process.myUid(), - resolveCallingPackage(), null), Settings.AUTHORITY, - Settings.CALL_METHOD_LIST_CONFIG, null, args); - if (b != null) { - Map<String, String> flagsToValues = - (HashMap) b.getSerializable(Settings.NameValueTable.VALUE); - allFlags.putAll(flagsToValues); + for (DeviceConfig.Properties properties : DeviceConfig.getAllProperties()) { + List<String> keys = new ArrayList<>(properties.getKeyset()); + for (String flagName : properties.getKeyset()) { + String fullName = properties.getNamespace() + "/" + flagName; + allFlags.put(fullName, properties.getString(flagName, null)); } - } catch (RemoteException e) { - throw new RuntimeException("Failed in IPC", e); } - return allFlags; } diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index a1f1a08d4bf2..e2ecda320065 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -591,13 +591,6 @@ flag { } flag { - name: "screenshot_shelf_ui2" - namespace: "systemui" - description: "Use new shelf UI flow for screenshots" - bug: "329659738" -} - -flag { name: "run_fingerprint_detect_on_dismissible_keyguard" namespace: "systemui" description: "Run fingerprint detect instead of authenticate if the keyguard is dismissible." diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index d0466313cf81..43d51c37aa36 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.Back -import com.android.compose.animation.scene.CommunalSwipeDetector import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.ElementMatcher diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index e2d693eb1089..f6535ec0b710 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -661,10 +661,7 @@ private fun Toolbar( val addWidgetText = stringResource(R.string.hub_mode_add_widget_button_text) ToolbarButton( isPrimary = !removeEnabled, - modifier = - Modifier.align(Alignment.CenterStart).semantics { - contentDescription = addWidgetText - }, + modifier = Modifier.align(Alignment.CenterStart), onClick = onOpenWidgetPicker, ) { Icon(Icons.Default.Add, null) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/CommunalSwipeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/systemui/communal/ui/compose/CommunalSwipeDetector.kt index 7be34cabfaf8..3fda9b85a5c2 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/CommunalSwipeDetector.kt +++ b/packages/SystemUI/compose/scene/src/com/android/systemui/communal/ui/compose/CommunalSwipeDetector.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.compose.animation.scene +package com.android.systemui.communal.ui.compose import androidx.compose.foundation.gestures.Orientation import androidx.compose.ui.input.pointer.PointerInputChange @@ -22,10 +22,12 @@ import androidx.compose.ui.input.pointer.positionChange import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize +import com.android.compose.animation.scene.Edge +import com.android.compose.animation.scene.SwipeDetector +import com.android.compose.animation.scene.SwipeSource +import com.android.compose.animation.scene.SwipeSourceDetector import kotlin.math.abs -private const val TRAVEL_RATIO_THRESHOLD = .5f - /** * {@link CommunalSwipeDetector} provides an implementation of {@link SwipeDetector} and {@link * SwipeSourceDetector} to enable fullscreen swipe handling to transition to and from the glanceable @@ -33,6 +35,10 @@ private const val TRAVEL_RATIO_THRESHOLD = .5f */ class CommunalSwipeDetector(private var lastDirection: SwipeSource? = null) : SwipeSourceDetector, SwipeDetector { + companion object { + private const val TRAVEL_RATIO_THRESHOLD = .5f + } + override fun source( layoutSize: IntSize, position: IntOffset, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt new file mode 100644 index 000000000000..e4916b1a7e46 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.domain.interactor + +import android.app.admin.DevicePolicyManager +import android.app.admin.devicePolicyManager +import android.content.Intent +import android.content.pm.UserInfo +import android.os.UserManager +import android.os.userManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.settings.fakeUserTracker +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.data.repository.fakeUserRepository +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalSettingsInteractorTest : SysuiTestCase() { + + private lateinit var userManager: UserManager + private lateinit var userRepository: FakeUserRepository + private lateinit var userTracker: FakeUserTracker + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private lateinit var underTest: CommunalSettingsInteractor + + @Before + fun setUp() { + userManager = kosmos.userManager + userRepository = kosmos.fakeUserRepository + userTracker = kosmos.fakeUserTracker + + val userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK) + userRepository.setUserInfos(userInfos) + userTracker.set( + userInfos = userInfos, + selectedUserIndex = 0, + ) + + underTest = kosmos.communalSettingsInteractor + } + + @Test + fun filterUsers_dontFilteredUsersWhenAllAreAllowed() = + testScope.runTest { + // If no users have any keyguard features disabled... + val disallowedUser by + collectLastValue(underTest.workProfileUserDisallowedByDevicePolicy) + // ...then the disallowed user should be null + assertNull(disallowedUser) + } + + @Test + fun filterUsers_filterWorkProfileUserWhenDisallowed() = + testScope.runTest { + // If the work profile user has keyguard widgets disabled... + setKeyguardFeaturesDisabled( + USER_INFO_WORK, + DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL + ) + // ...then the disallowed user match the work profile + val disallowedUser by + collectLastValue(underTest.workProfileUserDisallowedByDevicePolicy) + assertNotNull(disallowedUser) + assertEquals(USER_INFO_WORK.id, disallowedUser!!.id) + } + + private fun setKeyguardFeaturesDisabled(user: UserInfo, disabledFlags: Int) { + whenever( + kosmos.devicePolicyManager.getKeyguardDisabledFeatures( + anyOrNull(), + ArgumentMatchers.eq(user.id) + ) + ) + .thenReturn(disabledFlags) + kosmos.broadcastDispatcher.sendIntentToMatchingReceiversOnly( + context, + Intent(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED), + ) + } + + private companion object { + val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN) + val USER_INFO_WORK = + UserInfo( + 10, + "work", + /* iconPath= */ "", + /* flags= */ 0, + UserManager.USER_TYPE_PROFILE_MANAGED, + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.kt new file mode 100644 index 000000000000..1b704b4c4902 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.kt @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.util.service + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.dump.dumpManager +import com.android.systemui.testKosmos +import com.android.systemui.util.service.ObservableServiceConnection.DISCONNECT_REASON_DISCONNECTED +import com.android.systemui.util.time.fakeSystemClock +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify + +@SmallTest +@RunWith(AndroidJUnit4::class) +class PersistentConnectionManagerTest : SysuiTestCase() { + private val kosmos = testKosmos() + + private val fakeClock = kosmos.fakeSystemClock + private val fakeExecutor = kosmos.fakeExecutor + + private class Proxy { + // Fake proxy class + } + + private val connection: ObservableServiceConnection<Proxy> = mock() + private val observer: Observer = mock() + + private val underTest: PersistentConnectionManager<Proxy> by lazy { + PersistentConnectionManager( + /* clock = */ fakeClock, + /* bgExecutor = */ fakeExecutor, + /* dumpManager = */ kosmos.dumpManager, + /* dumpsysName = */ DUMPSYS_NAME, + /* serviceConnection = */ connection, + /* maxReconnectAttempts = */ MAX_RETRIES, + /* baseReconnectDelayMs = */ RETRY_DELAY_MS, + /* minConnectionDurationMs = */ CONNECTION_MIN_DURATION_MS, + /* observer = */ observer + ) + } + + /** Validates initial connection. */ + @Test + fun testConnect() { + underTest.start() + captureCallbackAndVerifyBind(connection).onConnected(connection, mock<Proxy>()) + } + + /** Ensures reconnection on disconnect. */ + @Test + fun testExponentialRetryOnDisconnect() { + underTest.start() + + // IF service is connected... + val captor = argumentCaptor<ObservableServiceConnection.Callback<Proxy>>() + verify(connection, times(1)).bind() + verify(connection).addCallback(captor.capture()) + val callback = captor.lastValue + callback.onConnected(connection, mock<Proxy>()) + + // ...AND service becomes disconnected within CONNECTION_MIN_DURATION_MS + callback.onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED) + + // THEN verify we retry to bind after the retry delay. (RETRY #1) + verify(connection, times(1)).bind() + fakeClock.advanceTime(RETRY_DELAY_MS.toLong()) + verify(connection, times(2)).bind() + + // IF service becomes disconnected for a second time after first retry... + callback.onConnected(connection, mock<Proxy>()) + callback.onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED) + + // THEN verify we retry after a longer delay of 2 * RETRY_DELAY_MS (RETRY #2) + fakeClock.advanceTime(RETRY_DELAY_MS.toLong()) + verify(connection, times(2)).bind() + fakeClock.advanceTime(RETRY_DELAY_MS.toLong()) + verify(connection, times(3)).bind() + + // IF service becomes disconnected for a third time after the second retry... + callback.onConnected(connection, mock<Proxy>()) + callback.onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED) + + // THEN verify we retry after a longer delay of 4 * RETRY_DELAY_MS (RETRY #3) + fakeClock.advanceTime(3 * RETRY_DELAY_MS.toLong()) + verify(connection, times(3)).bind() + fakeClock.advanceTime(RETRY_DELAY_MS.toLong()) + verify(connection, times(4)).bind() + } + + @Test + fun testDoesNotRetryAfterMaxRetries() { + underTest.start() + + val captor = argumentCaptor<ObservableServiceConnection.Callback<Proxy>>() + verify(connection).addCallback(captor.capture()) + val callback = captor.lastValue + + // IF we retry MAX_TRIES times... + for (attemptCount in 0 until MAX_RETRIES + 1) { + verify(connection, times(attemptCount + 1)).bind() + callback.onConnected(connection, mock<Proxy>()) + callback.onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED) + fakeClock.advanceTime(Math.scalb(RETRY_DELAY_MS.toDouble(), attemptCount).toLong()) + } + + // THEN we should not retry again after the last attempt. + fakeExecutor.advanceClockToLast() + verify(connection, times(MAX_RETRIES + 1)).bind() + } + + @Test + fun testEnsureNoRetryIfServiceNeverConnectsAfterRetry() { + underTest.start() + + with(captureCallbackAndVerifyBind(connection)) { + // IF service initially connects and then disconnects... + onConnected(connection, mock<Proxy>()) + onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED) + fakeExecutor.advanceClockToLast() + fakeExecutor.runAllReady() + + // ...AND we retry once. + verify(connection, times(1)).bind() + + // ...AND service disconnects after initial retry without ever connecting again. + onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED) + fakeExecutor.advanceClockToLast() + fakeExecutor.runAllReady() + + // THEN verify another retry is not triggered. + verify(connection, times(1)).bind() + } + } + + @Test + fun testEnsureNoRetryIfServiceNeverInitiallyConnects() { + underTest.start() + + with(captureCallbackAndVerifyBind(connection)) { + // IF service never connects and we just receive the disconnect signal... + onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED) + fakeExecutor.advanceClockToLast() + fakeExecutor.runAllReady() + + // THEN do not retry + verify(connection, never()).bind() + } + } + + /** Ensures manual unbind does not reconnect. */ + @Test + fun testStopDoesNotReconnect() { + underTest.start() + + val connectionCallbackCaptor = argumentCaptor<ObservableServiceConnection.Callback<Proxy>>() + verify(connection).addCallback(connectionCallbackCaptor.capture()) + verify(connection).bind() + clearInvocations(connection) + + underTest.stop() + fakeExecutor.advanceClockToNext() + fakeExecutor.runAllReady() + verify(connection, never()).bind() + } + + /** Ensures rebind on package change. */ + @Test + fun testAttemptOnPackageChange() { + underTest.start() + + verify(connection).bind() + + val callbackCaptor = argumentCaptor<Observer.Callback>() + captureCallbackAndVerifyBind(connection).onConnected(connection, mock<Proxy>()) + + verify(observer).addCallback(callbackCaptor.capture()) + callbackCaptor.lastValue.onSourceChanged() + verify(connection).bind() + } + + @Test + fun testAddConnectionCallback() { + val connectionCallback: ObservableServiceConnection.Callback<Proxy> = mock() + underTest.addConnectionCallback(connectionCallback) + verify(connection).addCallback(connectionCallback) + } + + @Test + fun testRemoveConnectionCallback() { + val connectionCallback: ObservableServiceConnection.Callback<Proxy> = mock() + underTest.removeConnectionCallback(connectionCallback) + verify(connection).removeCallback(connectionCallback) + } + + /** Helper method to capture the [ObservableServiceConnection.Callback] */ + private fun captureCallbackAndVerifyBind( + mConnection: ObservableServiceConnection<Proxy>, + ): ObservableServiceConnection.Callback<Proxy> { + + val connectionCallbackCaptor = argumentCaptor<ObservableServiceConnection.Callback<Proxy>>() + verify(mConnection).addCallback(connectionCallbackCaptor.capture()) + verify(mConnection).bind() + clearInvocations(mConnection) + + return connectionCallbackCaptor.lastValue + } + + companion object { + private const val MAX_RETRIES = 3 + private const val RETRY_DELAY_MS = 1000 + private const val CONNECTION_MIN_DURATION_MS = 5000 + private const val DUMPSYS_NAME = "dumpsys_name" + } +} diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml index 250076950907..65005f840598 100644 --- a/packages/SystemUI/res/layout/clipboard_overlay.xml +++ b/packages/SystemUI/res/layout/clipboard_overlay.xml @@ -24,18 +24,29 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:contentDescription="@string/clipboard_overlay_window_name"> - <ImageView + <!-- Min edge spacing guideline off of which the preview and actions can be anchored (without + this we'd need to express margins as the sum of two different dimens). --> + <androidx.constraintlayout.widget.Guideline + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/min_edge_guideline" + app:layout_constraintGuide_begin="@dimen/overlay_action_container_minimum_edge_spacing" + android:orientation="vertical"/> + <!-- Negative horizontal margin because this container background must render beyond the thing + it's constrained by (the actions themselves). --> + <FrameLayout android:id="@+id/actions_container_background" android:visibility="gone" android:layout_height="0dp" android:layout_width="0dp" android:elevation="4dp" - android:background="@drawable/action_chip_container_background" - android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" + android:background="@drawable/shelf_action_chip_container_background" + android:layout_marginStart="@dimen/negative_overlay_action_container_minimum_edge_spacing" + android:layout_marginEnd="@dimen/negative_overlay_action_container_minimum_edge_spacing" android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@+id/actions_container" - app:layout_constraintEnd_toEndOf="@+id/actions_container" + app:layout_constraintStart_toStartOf="@id/min_edge_guideline" + app:layout_constraintTop_toTopOf="@id/actions_container" + app:layout_constraintEnd_toEndOf="@id/actions_container" app:layout_constraintBottom_toBottomOf="parent"/> <HorizontalScrollView android:id="@+id/actions_container" @@ -56,10 +67,13 @@ android:id="@+id/actions" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:paddingStart="@dimen/shelf_action_chip_margin_start" + android:showDividers="middle" + android:divider="@drawable/shelf_action_chip_divider" android:animateLayoutChanges="true"> - <include layout="@layout/overlay_action_chip" + <include layout="@layout/shelf_action_chip" android:id="@+id/share_chip"/> - <include layout="@layout/overlay_action_chip" + <include layout="@layout/shelf_action_chip" android:id="@+id/remote_copy_chip"/> </LinearLayout> </HorizontalScrollView> @@ -73,7 +87,7 @@ android:layout_marginBottom="@dimen/overlay_preview_container_margin" android:elevation="7dp" android:background="@drawable/overlay_border" - app:layout_constraintStart_toStartOf="@id/actions_container_background" + app:layout_constraintStart_toStartOf="@id/min_edge_guideline" app:layout_constraintTop_toTopOf="@id/clipboard_preview" app:layout_constraintEnd_toEndOf="@id/clipboard_preview" app:layout_constraintBottom_toBottomOf="@id/actions_container_background"/> diff --git a/packages/SystemUI/res/layout/clipboard_overlay2.xml b/packages/SystemUI/res/layout/clipboard_overlay2.xml deleted file mode 100644 index 65005f840598..000000000000 --- a/packages/SystemUI/res/layout/clipboard_overlay2.xml +++ /dev/null @@ -1,202 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2021 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> -<com.android.systemui.clipboardoverlay.ClipboardOverlayView - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:id="@+id/clipboard_ui" - android:theme="@style/FloatingOverlay" - android:alpha="0" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:contentDescription="@string/clipboard_overlay_window_name"> - <!-- Min edge spacing guideline off of which the preview and actions can be anchored (without - this we'd need to express margins as the sum of two different dimens). --> - <androidx.constraintlayout.widget.Guideline - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:id="@+id/min_edge_guideline" - app:layout_constraintGuide_begin="@dimen/overlay_action_container_minimum_edge_spacing" - android:orientation="vertical"/> - <!-- Negative horizontal margin because this container background must render beyond the thing - it's constrained by (the actions themselves). --> - <FrameLayout - android:id="@+id/actions_container_background" - android:visibility="gone" - android:layout_height="0dp" - android:layout_width="0dp" - android:elevation="4dp" - android:background="@drawable/shelf_action_chip_container_background" - android:layout_marginStart="@dimen/negative_overlay_action_container_minimum_edge_spacing" - android:layout_marginEnd="@dimen/negative_overlay_action_container_minimum_edge_spacing" - android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" - app:layout_constraintStart_toStartOf="@id/min_edge_guideline" - app:layout_constraintTop_toTopOf="@id/actions_container" - app:layout_constraintEnd_toEndOf="@id/actions_container" - app:layout_constraintBottom_toBottomOf="parent"/> - <HorizontalScrollView - android:id="@+id/actions_container" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal" - android:paddingEnd="@dimen/overlay_action_container_padding_end" - android:paddingVertical="@dimen/overlay_action_container_padding_vertical" - android:elevation="4dp" - android:scrollbars="none" - app:layout_constraintHorizontal_bias="0" - app:layout_constraintWidth_percent="1.0" - app:layout_constraintWidth_max="wrap" - app:layout_constraintStart_toEndOf="@+id/preview_border" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintBottom_toBottomOf="@id/actions_container_background"> - <LinearLayout - android:id="@+id/actions" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:paddingStart="@dimen/shelf_action_chip_margin_start" - android:showDividers="middle" - android:divider="@drawable/shelf_action_chip_divider" - android:animateLayoutChanges="true"> - <include layout="@layout/shelf_action_chip" - android:id="@+id/share_chip"/> - <include layout="@layout/shelf_action_chip" - android:id="@+id/remote_copy_chip"/> - </LinearLayout> - </HorizontalScrollView> - <View - android:id="@+id/preview_border" - android:layout_width="0dp" - android:layout_height="0dp" - android:layout_marginStart="@dimen/overlay_preview_container_margin" - android:layout_marginTop="@dimen/overlay_border_width_neg" - android:layout_marginEnd="@dimen/overlay_border_width_neg" - android:layout_marginBottom="@dimen/overlay_preview_container_margin" - android:elevation="7dp" - android:background="@drawable/overlay_border" - app:layout_constraintStart_toStartOf="@id/min_edge_guideline" - app:layout_constraintTop_toTopOf="@id/clipboard_preview" - app:layout_constraintEnd_toEndOf="@id/clipboard_preview" - app:layout_constraintBottom_toBottomOf="@id/actions_container_background"/> - <FrameLayout - android:id="@+id/clipboard_preview" - android:layout_width="@dimen/clipboard_preview_size" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/overlay_border_width" - android:layout_marginBottom="@dimen/overlay_border_width" - android:layout_gravity="center" - android:elevation="7dp" - android:background="@drawable/overlay_preview_background" - android:clipChildren="true" - android:clipToOutline="true" - android:clipToPadding="true" - app:layout_constraintStart_toStartOf="@id/preview_border" - app:layout_constraintBottom_toBottomOf="@id/preview_border"> - <TextView android:id="@+id/text_preview" - android:textFontWeight="500" - android:padding="8dp" - android:gravity="center|start" - android:ellipsize="end" - android:autoSizeTextType="uniform" - android:autoSizeMinTextSize="@dimen/clipboard_overlay_min_font" - android:autoSizeMaxTextSize="@dimen/clipboard_overlay_max_font" - android:textColor="?attr/overlayButtonTextColor" - android:textColorLink="?attr/overlayButtonTextColor" - android:background="?androidprv:attr/colorAccentSecondary" - android:layout_width="@dimen/clipboard_preview_size" - android:layout_height="@dimen/clipboard_preview_size"/> - <ImageView - android:id="@+id/image_preview" - android:scaleType="fitCenter" - android:adjustViewBounds="true" - android:contentDescription="@string/clipboard_image_preview" - android:layout_width="match_parent" - android:layout_height="wrap_content"/> - <TextView - android:id="@+id/hidden_preview" - android:visibility="gone" - android:textFontWeight="500" - android:padding="8dp" - android:gravity="center" - android:textSize="14sp" - android:textColor="?attr/overlayButtonTextColor" - android:background="?androidprv:attr/colorAccentSecondary" - android:layout_width="@dimen/clipboard_preview_size" - android:layout_height="@dimen/clipboard_preview_size"/> - </FrameLayout> - <LinearLayout - android:id="@+id/minimized_preview" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:visibility="gone" - android:elevation="7dp" - android:padding="8dp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent" - android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" - android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" - android:background="@drawable/clipboard_minimized_background"> - <ImageView - android:src="@drawable/ic_content_paste" - android:tint="?attr/overlayButtonTextColor" - android:layout_width="24dp" - android:layout_height="24dp"/> - <ImageView - android:src="@*android:drawable/ic_chevron_end" - android:tint="?attr/overlayButtonTextColor" - android:layout_width="24dp" - android:layout_height="24dp" - android:paddingEnd="-8dp" - android:paddingStart="-4dp"/> - </LinearLayout> - <androidx.constraintlayout.widget.Barrier - android:id="@+id/clipboard_content_top" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="horizontal" - app:barrierDirection="top" - app:constraint_referenced_ids="clipboard_preview,minimized_preview"/> - <androidx.constraintlayout.widget.Barrier - android:id="@+id/clipboard_content_end" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical" - app:barrierDirection="end" - app:constraint_referenced_ids="clipboard_preview,minimized_preview"/> - <FrameLayout - android:id="@+id/dismiss_button" - android:layout_width="@dimen/overlay_dismiss_button_tappable_size" - android:layout_height="@dimen/overlay_dismiss_button_tappable_size" - android:elevation="10dp" - android:visibility="gone" - android:alpha="0" - app:layout_constraintStart_toEndOf="@id/clipboard_content_end" - app:layout_constraintEnd_toEndOf="@id/clipboard_content_end" - app:layout_constraintTop_toTopOf="@id/clipboard_content_top" - app:layout_constraintBottom_toTopOf="@id/clipboard_content_top" - android:contentDescription="@string/clipboard_dismiss_description"> - <ImageView - android:id="@+id/dismiss_image" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_margin="@dimen/overlay_dismiss_button_margin" - android:background="@drawable/circular_background" - android:backgroundTint="?androidprv:attr/materialColorPrimaryFixedDim" - android:tint="?androidprv:attr/materialColorOnPrimaryFixed" - android:padding="4dp" - android:src="@drawable/ic_close"/> - </FrameLayout> -</com.android.systemui.clipboardoverlay.ClipboardOverlayView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/screen_share_dialog.xml b/packages/SystemUI/res/layout/screen_share_dialog.xml index 2616e8ae25e8..aa083ad9fdea 100644 --- a/packages/SystemUI/res/layout/screen_share_dialog.xml +++ b/packages/SystemUI/res/layout/screen_share_dialog.xml @@ -46,7 +46,7 @@ android:layout_marginTop="@dimen/screenrecord_title_margin_top" android:gravity="center"/> <Spinner - android:id="@+id/screen_share_mode_spinner" + android:id="@+id/screen_share_mode_options" android:layout_width="match_parent" android:layout_height="@dimen/screenrecord_spinner_height" android:layout_marginTop="@dimen/screenrecord_spinner_margin" diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 0bc2c82538c8..dee55289958e 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -321,11 +321,11 @@ <!-- A toast message shown when the screen recording cannot be started due to a generic error [CHAR LIMIT=NONE] --> <string name="screenrecord_start_error">Error starting screen recording</string> <!-- Title for a dialog shown to the user that will let them stop recording their screen [CHAR LIMIT=50] --> - <string name="screenrecord_stop_dialog_title">Stop recording screen?</string> - <!-- Text telling a user that they will stop recording their screen if they click the "Stop recording" button [CHAR LIMIT=100] --> - <string name="screenrecord_stop_dialog_message">You will stop recording your screen</string> - <!-- Text telling a user that they will stop recording the contents of the specified [app_name] if they click the "Stop recording" button. Note that the app name will appear in bold. [CHAR LIMIT=100] --> - <string name="screenrecord_stop_dialog_message_specific_app">You will stop recording <b><xliff:g id="app_name" example="Photos App">%1$s</xliff:g></b></string> + <string name="screenrecord_stop_dialog_title">Stop recording?</string> + <!-- Text telling a user that they're currently recording their screen [CHAR LIMIT=100] --> + <string name="screenrecord_stop_dialog_message">You\'re currently recording your entire screen</string> + <!-- Text telling a user that they're currently recording the contents of the specified [app_name]. [CHAR LIMIT=100] --> + <string name="screenrecord_stop_dialog_message_specific_app">You\'re currently recording <xliff:g id="app_name" example="Photos App">%1$s</xliff:g></string> <!-- Button to stop a screen recording [CHAR LIMIT=35] --> <string name="screenrecord_stop_dialog_button">Stop recording</string> @@ -333,25 +333,33 @@ <string name="share_to_app_chip_accessibility_label">Sharing screen</string> <!-- Title for a dialog shown to the user that will let them stop sharing their screen to another app on the device [CHAR LIMIT=50] --> <string name="share_to_app_stop_dialog_title">Stop sharing screen?</string> - <!-- Text telling a user that they will stop sharing their screen if they click the "Stop sharing" button [CHAR LIMIT=100] --> - <string name="share_to_app_stop_dialog_message">You will stop sharing your screen</string> - <!-- Text telling a user that they will stop sharing the contents of the specified [app_name] if they click the "Stop sharing" button. Note that the app name will appear in bold. [CHAR LIMIT=100] --> - <string name="share_to_app_stop_dialog_message_specific_app">You will stop sharing <b><xliff:g id="app_name" example="Photos App">%1$s</xliff:g></b></string> + <!-- Text telling a user that they're currently sharing their entire screen to [host_app_name] (i.e. [host_app_name] can currently see all screen content) [CHAR LIMIT=150] --> + <string name="share_to_app_stop_dialog_message_entire_screen_with_host_app">You\'re currently sharing your entire screen with <xliff:g id="host_app_name" example="Screen Recorder App">%1$s</xliff:g></string> + <!-- Text telling a user that they're currently sharing their entire screen to an app (but we don't know what app) [CHAR LIMIT=150] --> + <string name="share_to_app_stop_dialog_message_entire_screen">You\'re currently sharing your entire screen with an app</string> + <!-- Text telling a user that they're currently sharing the contents of [app_being_shared_name]. (i.e. some app can currently see the content of [app_being_shared_name]). [CHAR LIMIT=150] --> + <string name="share_to_app_stop_dialog_message_single_app_specific">You\'re currently sharing <xliff:g id="app_being_shared_name" example="Photos App">%1$s</xliff:g></string> + <!-- Text telling a user that they're currently sharing their screen [CHAR LIMIT=150] --> + <string name="share_to_app_stop_dialog_message_single_app_generic">You\'re currently sharing an app</string> <!-- Button to stop screen sharing [CHAR LIMIT=35] --> <string name="share_to_app_stop_dialog_button">Stop sharing</string> <!-- Content description for the status bar chip shown to the user when they're casting their screen to a different device [CHAR LIMIT=NONE] --> <string name="cast_screen_to_other_device_chip_accessibility_label">Casting screen</string> - <!-- Title for a dialog shown to the user that will let them stop casting their screen to a different device [CHAR LIMIT=50] --> - <string name="cast_screen_to_other_device_stop_dialog_title">Stop casting screen?</string> <!-- Title for a dialog shown to the user that will let them stop casting to a different device [CHAR LIMIT=50] --> <string name="cast_to_other_device_stop_dialog_title">Stop casting?</string> - <!-- Text telling a user that they will stop casting their screen to a different device if they click the "Stop casting" button [CHAR LIMIT=100] --> - <string name="cast_screen_to_other_device_stop_dialog_message">You will stop casting your screen</string> - <!-- Text telling a user that they will stop casting the contents of the specified [app_name] to a different device if they click the "Stop casting" button. Note that the app name will appear in bold. [CHAR LIMIT=100] --> - <string name="cast_screen_to_other_device_stop_dialog_message_specific_app">You will stop casting <b><xliff:g id="app_name" example="Photos App">%1$s</xliff:g></b></string> - <!-- Text telling a user that they're currently casting to a different device [CHAR LIMIT=100] --> - <string name="cast_to_other_device_stop_dialog_message">You\'re currently casting</string> + <!-- Text telling a user that they're currently casting their screen to a different device. The device receiving the cast is named [device_name]. [CHAR LIMIT=150] --> + <string name="cast_to_other_device_stop_dialog_message_entire_screen_with_device">You\'re currently casting your entire screen to <xliff:g id="device_name" example="Living Room Device">%1$s</xliff:g></string> + <!-- Text telling a user that they're currently casting their screen to a nearby device. [CHAR LIMIT=150] --> + <string name="cast_to_other_device_stop_dialog_message_entire_screen">You\'re currently casting your entire screen to a nearby device</string> + <!-- Text telling a user that they're currently casting the contents of [app_being_shared_name] to a different device. The device receiving the cast is named [device_name]. [CHAR LIMIT=150] --> + <string name="cast_to_other_device_stop_dialog_message_specific_app_with_device">You\'re currently casting <xliff:g id="app_being_shared_name" example="Photos App">%1$s</xliff:g> to <xliff:g id="device_name" example="Living Room Device">%2$s</xliff:g></string> + <!-- Text telling a user that they're currently casting the contents of [app_being_shared_name] to a different device. [CHAR LIMIT=150] --> + <string name="cast_to_other_device_stop_dialog_message_specific_app">You\'re currently casting <xliff:g id="app_being_shared_name" example="Photos App">%1$s</xliff:g> to a nearby device</string> + <!-- Text telling a user that they're currently casting to a different device. The device receiving the cast is named [device_name]. [CHAR LIMIT=100] --> + <string name="cast_to_other_device_stop_dialog_message_generic_with_device">You\'re currently casting to <xliff:g id="device_name" example="Living Room Device">%1$s</xliff:g></string> + <!-- Text telling a user that they're currently casting to a nearby device [CHAR LIMIT=100] --> + <string name="cast_to_other_device_stop_dialog_message_generic">You\'re currently casting to a nearby device</string> <!-- Button to stop screen casting to a different device [CHAR LIMIT=35] --> <string name="cast_to_other_device_stop_dialog_button">Stop casting</string> diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java index ba236ba016ff..1762d82b3237 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java @@ -18,8 +18,6 @@ package com.android.systemui.clipboardoverlay; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static com.android.systemui.Flags.screenshotShelfUi2; - import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; @@ -61,7 +59,6 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import com.android.systemui.res.R; import com.android.systemui.screenshot.DraggableConstraintLayout; import com.android.systemui.screenshot.FloatingWindowUtil; -import com.android.systemui.screenshot.OverlayActionChip; import com.android.systemui.screenshot.ui.binder.ActionButtonViewBinder; import com.android.systemui.screenshot.ui.viewmodel.ActionButtonAppearance; import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel; @@ -152,66 +149,47 @@ public class ClipboardOverlayView extends DraggableConstraintLayout { } private void bindDefaultActionChips() { - if (screenshotShelfUi2()) { - mActionButtonViewBinder.bind(mRemoteCopyChip, - ActionButtonViewModel.Companion.withNextId( - new ActionButtonAppearance( - Icon.createWithResource(mContext, - R.drawable.ic_baseline_devices_24).loadDrawable( - mContext), - null, - mContext.getString(R.string.clipboard_send_nearby_description), - true), - new Function0<>() { - @Override - public Unit invoke() { - if (mClipboardCallbacks != null) { - mClipboardCallbacks.onRemoteCopyButtonTapped(); - } - return null; + mActionButtonViewBinder.bind(mRemoteCopyChip, + ActionButtonViewModel.Companion.withNextId( + new ActionButtonAppearance( + Icon.createWithResource(mContext, + R.drawable.ic_baseline_devices_24).loadDrawable( + mContext), + null, + mContext.getString(R.string.clipboard_send_nearby_description), + true), + new Function0<>() { + @Override + public Unit invoke() { + if (mClipboardCallbacks != null) { + mClipboardCallbacks.onRemoteCopyButtonTapped(); } - })); - mActionButtonViewBinder.bind(mShareChip, - ActionButtonViewModel.Companion.withNextId( - new ActionButtonAppearance( - Icon.createWithResource(mContext, - R.drawable.ic_screenshot_share).loadDrawable(mContext), - null, - mContext.getString(com.android.internal.R.string.share), - true), - new Function0<>() { - @Override - public Unit invoke() { - if (mClipboardCallbacks != null) { - mClipboardCallbacks.onShareButtonTapped(); - } - return null; + return null; + } + })); + mActionButtonViewBinder.bind(mShareChip, + ActionButtonViewModel.Companion.withNextId( + new ActionButtonAppearance( + Icon.createWithResource(mContext, + R.drawable.ic_screenshot_share).loadDrawable(mContext), + null, + mContext.getString(com.android.internal.R.string.share), + true), + new Function0<>() { + @Override + public Unit invoke() { + if (mClipboardCallbacks != null) { + mClipboardCallbacks.onShareButtonTapped(); } - })); - } else { - mShareChip.setAlpha(1); - mRemoteCopyChip.setAlpha(1); - - ((ImageView) mRemoteCopyChip.findViewById(R.id.overlay_action_chip_icon)).setImageIcon( - Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24)); - ((ImageView) mShareChip.findViewById(R.id.overlay_action_chip_icon)).setImageIcon( - Icon.createWithResource(mContext, R.drawable.ic_screenshot_share)); - - mShareChip.setContentDescription( - mContext.getString(com.android.internal.R.string.share)); - mRemoteCopyChip.setContentDescription( - mContext.getString(R.string.clipboard_send_nearby_description)); - } + return null; + } + })); } @Override public void setCallbacks(SwipeDismissCallbacks callbacks) { super.setCallbacks(callbacks); ClipboardOverlayCallbacks clipboardCallbacks = (ClipboardOverlayCallbacks) callbacks; - if (!screenshotShelfUi2()) { - mShareChip.setOnClickListener(v -> clipboardCallbacks.onShareButtonTapped()); - mRemoteCopyChip.setOnClickListener(v -> clipboardCallbacks.onRemoteCopyButtonTapped()); - } mDismissButton.setOnClickListener(v -> clipboardCallbacks.onDismissButtonTapped()); mClipboardPreview.setOnClickListener(v -> clipboardCallbacks.onPreviewTapped()); mMinimizedPreview.setOnClickListener(v -> clipboardCallbacks.onMinimizedViewTapped()); @@ -495,12 +473,7 @@ public class ClipboardOverlayView extends DraggableConstraintLayout { void setActionChip(RemoteAction action, Runnable onFinish) { mActionContainerBackground.setVisibility(View.VISIBLE); - View chip; - if (screenshotShelfUi2()) { - chip = constructShelfActionChip(action, onFinish); - } else { - chip = constructActionChip(action, onFinish); - } + View chip = constructShelfActionChip(action, onFinish); mActionContainer.addView(chip); mActionChips.add(chip); } @@ -534,17 +507,6 @@ public class ClipboardOverlayView extends DraggableConstraintLayout { return chip; } - private OverlayActionChip constructActionChip(RemoteAction action, Runnable onFinish) { - OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate( - R.layout.overlay_action_chip, mActionContainer, false); - chip.setText(action.getTitle()); - chip.setContentDescription(action.getTitle()); - chip.setIcon(action.getIcon(), false); - chip.setPendingIntent(action.getActionIntent(), onFinish); - chip.setAlpha(1); - return chip; - } - private static void updateTextSize(CharSequence text, TextView textView) { Paint paint = new Paint(textView.getPaint()); Resources res = textView.getResources(); diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java index 740a93eb081c..ff9fba4c03f1 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java @@ -18,8 +18,6 @@ package com.android.systemui.clipboardoverlay.dagger; import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; -import static com.android.systemui.Flags.screenshotShelfUi2; - import static java.lang.annotation.RetentionPolicy.RUNTIME; import android.content.Context; @@ -59,13 +57,8 @@ public interface ClipboardOverlayModule { */ @Provides static ClipboardOverlayView provideClipboardOverlayView(@OverlayWindowContext Context context) { - if (screenshotShelfUi2()) { - return (ClipboardOverlayView) LayoutInflater.from(context).inflate( - R.layout.clipboard_overlay2, null); - } else { - return (ClipboardOverlayView) LayoutInflater.from(context).inflate( - R.layout.clipboard_overlay, null); - } + return (ClipboardOverlayView) LayoutInflater.from(context).inflate( + R.layout.clipboard_overlay, null); } @Qualifier diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index f5255ac4d545..86f5fe1cac57 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -20,6 +20,7 @@ import android.app.smartspace.SmartspaceTarget import android.content.ComponentName import android.content.Intent import android.content.IntentFilter +import android.content.pm.UserInfo import android.os.UserHandle import android.os.UserManager import android.provider.Settings @@ -386,11 +387,11 @@ constructor( combine( widgetRepository.communalWidgets .map { filterWidgetsByExistingUsers(it) } - .combine(communalSettingsInteractor.allowedByDevicePolicyForWorkProfile) { + .combine(communalSettingsInteractor.workProfileUserDisallowedByDevicePolicy) { // exclude widgets under work profile if not allowed by device policy widgets, - allowedForWorkProfile -> - filterWidgetsAllowedByDevicePolicy(widgets, allowedForWorkProfile) + disallowedByPolicyUser -> + filterWidgetsAllowedByDevicePolicy(widgets, disallowedByPolicyUser) }, updateOnWorkProfileBroadcastReceived, ) { widgets, _ -> @@ -418,13 +419,11 @@ constructor( /** Filter widgets based on whether their associated profile is allowed by device policy. */ private fun filterWidgetsAllowedByDevicePolicy( list: List<CommunalWidgetContentModel>, - allowedByDevicePolicyForWorkProfile: Boolean + disallowedByDevicePolicyUser: UserInfo? ): List<CommunalWidgetContentModel> = - if (allowedByDevicePolicyForWorkProfile) { + if (disallowedByDevicePolicyUser == null) { list } else { - // Get associated work profile for the currently selected user. - val workProfile = userTracker.userProfiles.find { it.isManagedProfile } list.filter { model -> val uid = when (model) { @@ -432,7 +431,7 @@ constructor( model.providerInfo.profile.identifier is CommunalWidgetContentModel.Pending -> model.user.identifier } - uid != workProfile?.id + uid != disallowedByDevicePolicyUser.id } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt index 47b75c458d20..3b01aec6861d 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt @@ -92,15 +92,22 @@ constructor( awaitClose { userTracker.removeCallback(callback) } } - /** Whether or not keyguard widgets are allowed for work profile by device policy manager. */ - val allowedByDevicePolicyForWorkProfile: StateFlow<Boolean> = + /** + * A user that device policy says shouldn't allow communal widgets, or null if there are no + * restrictions. + */ + val workProfileUserDisallowedByDevicePolicy: StateFlow<UserInfo?> = workProfileUserInfoCallbackFlow .flatMapLatest { workProfile -> - workProfile?.let { repository.getAllowedByDevicePolicy(it) } ?: flowOf(false) + workProfile?.let { + repository.getAllowedByDevicePolicy(it).map { allowed -> + if (!allowed) it else null + } + } ?: flowOf(null) } .stateIn( scope = bgScope, started = SharingStarted.WhileSubscribed(), - initialValue = false + initialValue = null ) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt index 4f54fee3f498..18b343eb97c4 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -186,6 +186,10 @@ constructor( AppWidgetManager.EXTRA_CATEGORY_FILTER, CommunalWidgetCategories.defaultCategories ) + + communalSettingsInteractor.workProfileUserDisallowedByDevicePolicy.value?.let { + putExtra(EXTRA_USER_ID_FILTER, arrayListOf(it.id)) + } putExtra(EXTRA_UI_SURFACE_KEY, EXTRA_UI_SURFACE_VALUE) putExtra(EXTRA_PICKER_TITLE, resources.getString(R.string.communal_widget_picker_title)) putExtra( @@ -223,6 +227,7 @@ constructor( private const val EXTRA_PICKER_DESCRIPTION = "picker_description" private const val EXTRA_UI_SURFACE_KEY = "ui_surface" private const val EXTRA_UI_SURFACE_VALUE = "widgets_hub" + private const val EXTRA_USER_ID_FILTER = "filtered_user_ids" const val EXTRA_ADDED_APP_WIDGETS_KEY = "added_app_widgets" } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt index 297ad847caa6..befd822e14cd 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt @@ -161,5 +161,6 @@ constructor( TASK_FRAGMENT_TRANSIT_CLOSE, false ) + organizer.unregisterOrganizer() } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index c4b70d8013bf..9f3311373709 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -81,6 +81,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardWakeDirectlyToGoneInteractor; import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindParamsApplier; import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindViewBinder; import com.android.systemui.keyguard.ui.binder.WindowManagerLockscreenVisibilityViewBinder; @@ -317,7 +318,7 @@ public class KeyguardService extends Service { private final WindowManagerOcclusionManager mWmOcclusionManager; private final KeyguardEnabledInteractor mKeyguardEnabledInteractor; - + private final KeyguardWakeDirectlyToGoneInteractor mKeyguardWakeDirectlyToGoneInteractor; private final Lazy<FoldGracePeriodProvider> mFoldGracePeriodProvider = new Lazy<>() { @Override public FoldGracePeriodProvider get() { @@ -344,7 +345,8 @@ public class KeyguardService extends Service { @Main Executor mainExecutor, KeyguardInteractor keyguardInteractor, KeyguardEnabledInteractor keyguardEnabledInteractor, - Lazy<KeyguardStateCallbackStartable> keyguardStateCallbackStartableLazy) { + Lazy<KeyguardStateCallbackStartable> keyguardStateCallbackStartableLazy, + KeyguardWakeDirectlyToGoneInteractor keyguardWakeDirectlyToGoneInteractor) { super(); mKeyguardViewMediator = keyguardViewMediator; mKeyguardLifecyclesDispatcher = keyguardLifecyclesDispatcher; @@ -372,6 +374,7 @@ public class KeyguardService extends Service { mWmOcclusionManager = windowManagerOcclusionManager; mKeyguardEnabledInteractor = keyguardEnabledInteractor; + mKeyguardWakeDirectlyToGoneInteractor = keyguardWakeDirectlyToGoneInteractor; } @Override @@ -486,6 +489,7 @@ public class KeyguardService extends Service { public void onDreamingStarted() { trace("onDreamingStarted"); checkPermission(); + mKeyguardWakeDirectlyToGoneInteractor.onDreamingStarted(); mKeyguardInteractor.setDreaming(true); mKeyguardViewMediator.onDreamingStarted(); } @@ -494,6 +498,7 @@ public class KeyguardService extends Service { public void onDreamingStopped() { trace("onDreamingStopped"); checkPermission(); + mKeyguardWakeDirectlyToGoneInteractor.onDreamingStopped(); mKeyguardInteractor.setDreaming(false); mKeyguardViewMediator.onDreamingStopped(); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 608e25a82eff..33f9209fea09 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -42,6 +42,7 @@ import com.android.systemui.CoreStartable import com.android.systemui.biometrics.ui.binder.DeviceEntryUnlockTrackerViewBinder import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor @@ -73,6 +74,7 @@ import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import dagger.Lazy import java.util.Optional import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -108,6 +110,7 @@ constructor( private val clockInteractor: KeyguardClockInteractor, private val keyguardViewMediator: KeyguardViewMediator, private val deviceEntryUnlockTrackerViewBinder: Optional<DeviceEntryUnlockTrackerViewBinder>, + @Main private val mainDispatcher: CoroutineDispatcher, ) : CoreStartable { private var rootViewHandle: DisposableHandle? = null @@ -215,6 +218,7 @@ constructor( vibratorHelper, falsingManager, keyguardViewMediator, + mainDispatcher, ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index f837d8efdcc5..ae751dbacd68 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -127,6 +127,30 @@ interface KeyguardRepository { */ val isKeyguardEnabled: StateFlow<Boolean> + /** + * Whether we can transition directly back to GONE from AOD/DOZING without any authentication + * events (such as a fingerprint wake and unlock), even though authentication would normally be + * required. This means that if you tap the screen or press the power button, you'll return + * directly to the unlocked app content without seeing the lockscreen, even if a secure + * authentication method (PIN/password/biometrics) is set. + * + * This is true in these cases: + * - The screen timed out, but the "lock after screen timeout" duration (default 5 seconds) has + * not yet elapsed. + * - The power button was pressed, but "power button instantly locks" is not enabled, and the + * "lock after screen timeout" duration has not elapsed. + * + * Note that this value specifically tells us if we can *ignore* authentication that would + * otherwise be required to transition from AOD/DOZING -> GONE. AOD/DOZING -> GONE is also + * possible if keyguard is disabled, either from an app request or because security is set to + * "none", but in that case, auth is not required so this boolean is not relevant. + * + * See [KeyguardWakeToGoneInteractor]. + */ + val canIgnoreAuthAndReturnToGone: StateFlow<Boolean> + + fun setCanIgnoreAuthAndReturnToGone(canWake: Boolean) + /** Is the always-on display available to be used? */ val isAodAvailable: StateFlow<Boolean> @@ -386,6 +410,13 @@ constructor( MutableStateFlow(!lockPatternUtils.isLockScreenDisabled(userTracker.userId)) override val isKeyguardEnabled: StateFlow<Boolean> = _isKeyguardEnabled.asStateFlow() + private val _canIgnoreAuthAndReturnToGone = MutableStateFlow(false) + override val canIgnoreAuthAndReturnToGone = _canIgnoreAuthAndReturnToGone.asStateFlow() + + override fun setCanIgnoreAuthAndReturnToGone(canWakeToGone: Boolean) { + _canIgnoreAuthAndReturnToGone.value = canWakeToGone + } + private val _isDozing = MutableStateFlow(statusBarStateController.isDozing) override val isDozing: StateFlow<Boolean> = _isDozing.asStateFlow() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt index 868c4629dbb3..1167cc47448c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt @@ -53,6 +53,7 @@ constructor( powerInteractor: PowerInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, val deviceEntryRepository: DeviceEntryRepository, + private val wakeToGoneInteractor: KeyguardWakeDirectlyToGoneInteractor, ) : TransitionInteractor( fromState = KeyguardState.AOD, @@ -98,6 +99,7 @@ constructor( keyguardInteractor.primaryBouncerShowing, keyguardInteractor.isKeyguardOccluded, canDismissLockscreen, + wakeToGoneInteractor.canWakeDirectlyToGone, ) .collect { ( @@ -107,6 +109,7 @@ constructor( primaryBouncerShowing, isKeyguardOccludedLegacy, canDismissLockscreen, + canWakeDirectlyToGone, ) -> if (!maybeHandleInsecurePowerGesture()) { val shouldTransitionToLockscreen = @@ -131,8 +134,7 @@ constructor( val shouldTransitionToGone = (!KeyguardWmStateRefactor.isEnabled && canDismissLockscreen) || - (KeyguardWmStateRefactor.isEnabled && - !deviceEntryRepository.isLockscreenEnabled()) + (KeyguardWmStateRefactor.isEnabled && canWakeDirectlyToGone) if (shouldTransitionToGone) { startTransitionTo( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index 76e88a2a2cd6..aee65a81880d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -55,6 +55,7 @@ constructor( private val communalInteractor: CommunalInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, val deviceEntryRepository: DeviceEntryRepository, + private val wakeToGoneInteractor: KeyguardWakeDirectlyToGoneInteractor, ) : TransitionInteractor( fromState = KeyguardState.DOZING, @@ -181,7 +182,7 @@ constructor( .sample( communalInteractor.isIdleOnCommunal, keyguardInteractor.biometricUnlockState, - canTransitionToGoneOnWake, + wakeToGoneInteractor.canWakeDirectlyToGone, keyguardInteractor.primaryBouncerShowing, ) .collect { @@ -189,27 +190,14 @@ constructor( _, isIdleOnCommunal, biometricUnlockState, - canDismissLockscreen, + canWakeDirectlyToGone, primaryBouncerShowing) -> if ( !maybeStartTransitionToOccludedOrInsecureCamera() && // Handled by dismissFromDozing(). !isWakeAndUnlock(biometricUnlockState.mode) ) { - if (!KeyguardWmStateRefactor.isEnabled && canDismissLockscreen) { - if (SceneContainerFlag.isEnabled) { - // TODO(b/336576536): Check if adaptation for scene framework is - // needed - } else { - startTransitionTo( - KeyguardState.GONE, - ownerReason = "waking from dozing" - ) - } - } else if ( - KeyguardWmStateRefactor.isEnabled && - !deviceEntryRepository.isLockscreenEnabled() - ) { + if (canWakeDirectlyToGone) { if (SceneContainerFlag.isEnabled) { // TODO(b/336576536): Check if adaptation for scene framework is // needed diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 0e764879d8f6..cfb161cd3d33 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -23,6 +23,7 @@ import com.android.systemui.Flags.communalHub import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockMode @@ -37,11 +38,14 @@ import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class FromDreamingTransitionInteractor @Inject @@ -56,6 +60,7 @@ constructor( private val glanceableHubTransitions: GlanceableHubTransitions, powerInteractor: PowerInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, + private val deviceEntryInteractor: DeviceEntryInteractor, ) : TransitionInteractor( fromState = KeyguardState.DREAMING, @@ -72,7 +77,7 @@ constructor( listenForDreamingToOccluded() listenForDreamingToGoneWhenDismissable() listenForDreamingToGoneFromBiometricUnlock() - listenForDreamingToLockscreen() + listenForDreamingToLockscreenOrGone() listenForDreamingToAodOrDozing() listenForTransitionToCamera(scope, keyguardInteractor) listenForDreamingToGlanceableHub() @@ -132,17 +137,7 @@ constructor( @OptIn(FlowPreview::class) private fun listenForDreamingToOccluded() { - if (KeyguardWmStateRefactor.isEnabled) { - scope.launch { - combine( - keyguardInteractor.isDreaming, - keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop, - ::Pair - ) - .filterRelevantKeyguardStateAnd { (isDreaming, _) -> !isDreaming } - .collect { maybeStartTransitionToOccludedOrInsecureCamera() } - } - } else { + if (!KeyguardWmStateRefactor.isEnabled) { scope.launch { combine( keyguardInteractor.isKeyguardOccluded, @@ -168,21 +163,41 @@ constructor( } } - private fun listenForDreamingToLockscreen() { + private fun listenForDreamingToLockscreenOrGone() { if (!KeyguardWmStateRefactor.isEnabled) { return } scope.launch { - keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop - .filterRelevantKeyguardStateAnd { onTop -> !onTop } - .collect { startTransitionTo(KeyguardState.LOCKSCREEN) } + keyguardInteractor.isDreaming + .filter { !it } + .sample(deviceEntryInteractor.isUnlocked, ::Pair) + .collect { (_, dismissable) -> + // TODO(b/349837588): Add check for -> OCCLUDED. + if (dismissable) { + startTransitionTo( + KeyguardState.GONE, + ownerReason = "No longer dreaming; dismissable" + ) + } else { + startTransitionTo( + KeyguardState.LOCKSCREEN, + ownerReason = "No longer dreaming" + ) + } + } } } private fun listenForDreamingToGoneWhenDismissable() { - // TODO(b/336576536): Check if adaptation for scene framework is needed - if (SceneContainerFlag.isEnabled) return + if (SceneContainerFlag.isEnabled) { + return // TODO(b/336576536): Check if adaptation for scene framework is needed + } + + if (KeyguardWmStateRefactor.isEnabled) { + return + } + scope.launch { keyguardInteractor.isAbleToDream .sampleCombine( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 859326a29b28..ec03a6d8121f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -52,7 +52,6 @@ import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.notification.NotificationUtils.interpolate import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine -import com.android.systemui.util.kotlin.Utils.Companion.sampleFilter import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.kotlin.sample import javax.inject.Inject @@ -251,13 +250,17 @@ constructor( /** Keyguard can be clipped at the top as the shade is dragged */ val topClippingBounds: Flow<Int?> by lazy { - repository.topClippingBounds - .sampleFilter( + combineTransform( keyguardTransitionInteractor .transitionValue(scene = Scenes.Gone, stateWithoutSceneContainer = GONE) - .onStart { emit(0f) } - ) { goneValue -> - goneValue != 1f + .map { it == 1f } + .onStart { emit(false) } + .distinctUntilChanged(), + repository.topClippingBounds + ) { isGone, topClippingBounds -> + if (!isGone) { + emit(topClippingBounds) + } } .distinctUntilChanged() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt new file mode 100644 index 000000000000..6a1b7cfb7d7e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt @@ -0,0 +1,367 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.annotation.SuppressLint +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.provider.Settings +import android.provider.Settings.Secure +import com.android.internal.widget.LockPatternUtils +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.KeyguardViewMediator +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.keyguard.shared.model.BiometricUnlockMode +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.KeyguardState.Companion.deviceIsAsleepInState +import com.android.systemui.keyguard.shared.model.KeyguardState.Companion.deviceIsAwakeInState +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import com.android.systemui.util.kotlin.sample +import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.SystemSettings +import com.android.systemui.util.time.SystemClock +import javax.inject.Inject +import kotlin.math.max +import kotlin.math.min +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +/** + * Logic related to the ability to wake directly to GONE from asleep (AOD/DOZING), without going + * through LOCKSCREEN or a BOUNCER state. + * + * This is possible in the following scenarios: + * - The lockscreen is disabled, either from an app request (SUW does this), or by the security + * "None" setting. + * - A biometric authentication event occurred while we were asleep (fingerprint auth, etc). This + * specifically is referred to throughout the codebase as "wake and unlock". + * - The screen timed out, but the "lock after screen timeout" duration has not elapsed. + * - The power button was pressed, but "power button instantly locks" is disabled and the "lock + * after screen timeout" duration has not elapsed. + * + * In these cases, no (further) authentication is required, and we can transition directly from + * AOD/DOZING -> GONE. + */ +@SysUISingleton +class KeyguardWakeDirectlyToGoneInteractor +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val context: Context, + private val repository: KeyguardRepository, + private val systemClock: SystemClock, + private val alarmManager: AlarmManager, + private val transitionInteractor: KeyguardTransitionInteractor, + private val powerInteractor: PowerInteractor, + private val secureSettings: SecureSettings, + private val lockPatternUtils: LockPatternUtils, + private val systemSettings: SystemSettings, + private val selectedUserInteractor: SelectedUserInteractor, +) { + + /** + * Whether the lockscreen was disabled as of the last wake/sleep event, according to + * LockPatternUtils. + * + * This will always be true if [repository.isKeyguardServiceEnabled]=false, but it can also be + * true when the keyguard service is enabled if the lockscreen has been disabled via adb using + * the `adb shell locksettings set-disabled true` command, which is often done in tests. + * + * Unlike keyguardServiceEnabled, changes to this value should *not* immediately show or hide + * the keyguard. If the lockscreen is disabled in this way, it will just not show on the next + * sleep/wake. + */ + private val isLockscreenDisabled: Flow<Boolean> = + powerInteractor.isAwake.map { isLockscreenDisabled() } + + /** + * Whether we can wake from AOD/DOZING directly to GONE, bypassing LOCKSCREEN/BOUNCER states. + * + * This is possible in the following cases: + * - Keyguard is disabled, either from an app request or from security being set to "None". + * - We're wake and unlocking (fingerprint auth occurred while asleep). + * - We're allowed to ignore auth and return to GONE, due to timeouts not elapsing. + */ + val canWakeDirectlyToGone = + combine( + repository.isKeyguardEnabled, + isLockscreenDisabled, + repository.biometricUnlockState, + repository.canIgnoreAuthAndReturnToGone, + ) { + keyguardEnabled, + isLockscreenDisabled, + biometricUnlockState, + canIgnoreAuthAndReturnToGone -> + (!keyguardEnabled || isLockscreenDisabled) || + BiometricUnlockMode.isWakeAndUnlock(biometricUnlockState.mode) || + canIgnoreAuthAndReturnToGone + } + .distinctUntilChanged() + + /** + * Counter that is incremented every time we wake up or stop dreaming. Upon sleeping/dreaming, + * we put the current value of this counter into the intent extras of the timeout alarm intent. + * If this value has changed by the time we receive the intent, it is discarded since it's out + * of date. + */ + var timeoutCounter = 0 + + var isAwake = false + + private val broadcastReceiver: BroadcastReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (DELAYED_KEYGUARD_ACTION == intent.action) { + val sequence = intent.getIntExtra(SEQ_EXTRA_KEY, 0) + synchronized(this) { + if (timeoutCounter == sequence) { + // If the sequence # matches, we have not woken up or stopped dreaming + // since + // the alarm was set. That means this is still relevant - the lock + // timeout + // has elapsed, so let the repository know that we can no longer return + // to + // GONE without authenticating. + repository.setCanIgnoreAuthAndReturnToGone(false) + } + } + } + } + } + + init { + setOrCancelAlarmFromWakefulness() + listenForWakeToClearCanIgnoreAuth() + registerBroadcastReceiver() + } + + fun onDreamingStarted() { + // If we start dreaming while awake, lock after the normal timeout. + if (isAwake) { + setResetCanIgnoreAuthAlarm() + } + } + + fun onDreamingStopped() { + // Cancel the timeout if we stop dreaming while awake. + if (isAwake) { + cancelCanIgnoreAuthAlarm() + } + } + + private fun setOrCancelAlarmFromWakefulness() { + scope.launch { + powerInteractor.detailedWakefulness + .distinctUntilChangedBy { it.isAwake() } + .sample(transitionInteractor.currentKeyguardState, ::Pair) + .collect { (wakefulness, currentState) -> + // Save isAwake for use in onDreamingStarted/onDreamingStopped. + this@KeyguardWakeDirectlyToGoneInteractor.isAwake = wakefulness.isAwake() + + // If we're sleeping from GONE, check the timeout and lock instantly settings. + // These are not relevant if we're coming from non-GONE states. + if (!isAwake && currentState == KeyguardState.GONE) { + val lockTimeoutDuration = getCanIgnoreAuthAndReturnToGoneDuration() + + // If the screen timed out and went to sleep, and the lock timeout is > 0ms, + // then we can return to GONE until that duration elapses. If the power + // button was pressed but "instantly locks" is disabled, then we can also + // return to GONE until the timeout duration elapses. + if ( + (wakefulness.lastSleepReason == WakeSleepReason.TIMEOUT && + lockTimeoutDuration > 0) || + (wakefulness.lastSleepReason == WakeSleepReason.POWER_BUTTON && + !willLockImmediately()) + ) { + + // Let the repository know that we can return to GONE until we notify + // it otherwise. + repository.setCanIgnoreAuthAndReturnToGone(true) + setResetCanIgnoreAuthAlarm() + } + } else if (isAwake) { + // If we're waking up, ignore the alarm if it goes off since it's no longer + // relevant. Once a wake KeyguardTransition is started, we'll also clear the + // canIgnoreAuthAndReturnToGone value in listenForWakeToClearCanIgnoreAuth. + cancelCanIgnoreAuthAlarm() + } + } + } + } + + /** Clears the canIgnoreAuthAndReturnToGone value upon waking. */ + private fun listenForWakeToClearCanIgnoreAuth() { + scope.launch { + transitionInteractor + .isInTransitionWhere( + fromStatePredicate = { deviceIsAsleepInState(it) }, + toStatePredicate = { deviceIsAwakeInState(it) }, + ) + .collect { + // This value is reset when the timeout alarm fires, but if the device is woken + // back up before then, it needs to be reset here. The alarm is cancelled + // immediately upon waking up, but since this value is used by keyguard + // transition internals to decide whether we can transition to GONE, wait until + // that decision is made before resetting it. + repository.setCanIgnoreAuthAndReturnToGone(false) + } + } + } + + /** + * Registers the broadcast receiver to receive the alarm intent. + * + * TODO(b/351817381): Investigate using BroadcastDispatcher vs. ignoring this lint warning. + */ + @SuppressLint("WrongConstant", "RegisterReceiverViaContext") + private fun registerBroadcastReceiver() { + val delayedActionFilter = IntentFilter() + delayedActionFilter.addAction(KeyguardViewMediator.DELAYED_KEYGUARD_ACTION) + // TODO(b/346803756): Listen for DELAYED_LOCK_PROFILE_ACTION. + delayedActionFilter.priority = IntentFilter.SYSTEM_HIGH_PRIORITY + context.registerReceiver( + broadcastReceiver, + delayedActionFilter, + SYSTEMUI_PERMISSION, + null /* scheduler */, + Context.RECEIVER_EXPORTED_UNAUDITED + ) + } + + /** Set an alarm for */ + private fun setResetCanIgnoreAuthAlarm() { + val intent = + Intent(DELAYED_KEYGUARD_ACTION).apply { + setPackage(context.packageName) + putExtra(SEQ_EXTRA_KEY, timeoutCounter) + addFlags(Intent.FLAG_RECEIVER_FOREGROUND) + } + + val sender = + PendingIntent.getBroadcast( + context, + 0, + intent, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val time = systemClock.elapsedRealtime() + getCanIgnoreAuthAndReturnToGoneDuration() + alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, time, sender) + + // TODO(b/346803756): Migrate support for child profiles. + } + + /** + * Cancel the timeout by incrementing the counter so that we ignore the intent when it's + * received. + */ + private fun cancelCanIgnoreAuthAlarm() { + timeoutCounter++ + } + + /** + * Whether pressing the power button locks the device immediately; vs. waiting for a specified + * timeout first. + */ + private fun willLockImmediately( + userId: Int = selectedUserInteractor.getSelectedUserId() + ): Boolean { + return lockPatternUtils.getPowerButtonInstantlyLocks(userId) || + !lockPatternUtils.isSecure(userId) + } + + /** + * Returns whether the lockscreen is disabled, either because the keyguard service is disabled + * or because an adb command has disabled the lockscreen. + */ + private fun isLockscreenDisabled( + userId: Int = selectedUserInteractor.getSelectedUserId() + ): Boolean { + return lockPatternUtils.isLockScreenDisabled(userId) + } + + /** + * Returns the duration within which we can return to GONE without auth after a screen timeout + * (or power button press, if lock instantly is disabled). + * + * This takes into account the user's settings as well as device policy maximums. + */ + private fun getCanIgnoreAuthAndReturnToGoneDuration( + userId: Int = selectedUserInteractor.getSelectedUserId() + ): Long { + // The timeout duration from settings (Security > Device Unlock > Gear icon > "Lock after + // screen timeout". + val durationSetting: Long = + secureSettings + .getIntForUser( + Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, + KEYGUARD_CAN_IGNORE_AUTH_DURATION, + userId + ) + .toLong() + + // Device policy maximum timeout. + val durationDevicePolicyMax = + lockPatternUtils.devicePolicyManager.getMaximumTimeToLock(null, userId) + + return if (durationDevicePolicyMax <= 0) { + durationSetting + } else { + var displayTimeout = + systemSettings + .getIntForUser( + Settings.System.SCREEN_OFF_TIMEOUT, + KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT, + userId + ) + .toLong() + + // Ignore negative values. I don't know why this would be negative, but this check has + // been around since 2016 and I see no upside to removing it. + displayTimeout = max(displayTimeout, 0) + + // Respect the shorter of: the device policy (maximum duration between last user action + // and fully locking) or the "Lock after screen timeout" setting. + max(min(durationDevicePolicyMax - displayTimeout, durationSetting), 0) + } + } + + companion object { + private const val DELAYED_KEYGUARD_ACTION = + "com.android.internal.policy.impl.PhoneWindowManager.DELAYED_KEYGUARD" + private const val DELAYED_LOCK_PROFILE_ACTION = + "com.android.internal.policy.impl.PhoneWindowManager.DELAYED_LOCK" + private const val SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF" + private const val SEQ_EXTRA_KEY = "count" + + private const val KEYGUARD_CAN_IGNORE_AUTH_DURATION = 5000 + private const val KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT = 30000 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt index 3355ffd83138..0985e69ff8ff 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt @@ -21,14 +21,17 @@ package com.android.systemui.keyguard.domain.interactor import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockMode import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.KeyguardState.Companion.deviceIsAsleepInState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor +import com.android.systemui.util.kotlin.Utils.Companion.toTriple import com.android.systemui.util.kotlin.sample import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import dagger.Lazy @@ -41,11 +44,13 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class WindowManagerLockscreenVisibilityInteractor @Inject constructor( keyguardInteractor: KeyguardInteractor, + transitionRepository: KeyguardTransitionRepository, transitionInteractor: KeyguardTransitionInteractor, surfaceBehindInteractor: KeyguardSurfaceBehindInteractor, fromLockscreenInteractor: FromLockscreenTransitionInteractor, @@ -54,9 +59,15 @@ constructor( notificationLaunchAnimationInteractor: NotificationLaunchAnimationInteractor, sceneInteractor: Lazy<SceneInteractor>, deviceEntryInteractor: Lazy<DeviceEntryInteractor>, + wakeToGoneInteractor: KeyguardWakeDirectlyToGoneInteractor, ) { private val defaultSurfaceBehindVisibility = - transitionInteractor.finishedKeyguardState.map(::isSurfaceVisible) + combine( + transitionInteractor.finishedKeyguardState, + wakeToGoneInteractor.canWakeDirectlyToGone, + ) { finishedState, canWakeDirectlyToGone -> + isSurfaceVisible(finishedState) || canWakeDirectlyToGone + } /** * Surface visibility provided by the From*TransitionInteractor responsible for the currently @@ -203,9 +214,13 @@ constructor( if (SceneContainerFlag.isEnabled) { isDeviceNotEntered } else { - transitionInteractor.currentKeyguardState - .sample(transitionInteractor.startedStepWithPrecedingStep, ::Pair) - .map { (currentState, startedWithPrev) -> + combine( + transitionInteractor.currentKeyguardState, + wakeToGoneInteractor.canWakeDirectlyToGone, + ::Pair + ) + .sample(transitionInteractor.startedStepWithPrecedingStep, ::toTriple) + .map { (currentState, canWakeDirectlyToGone, startedWithPrev) -> val startedFromStep = startedWithPrev.previousValue val startedStep = startedWithPrev.newValue val returningToGoneAfterCancellation = @@ -213,16 +228,33 @@ constructor( startedFromStep.transitionState == TransitionState.CANCELED && startedFromStep.from == KeyguardState.GONE - if (!returningToGoneAfterCancellation) { - // By default, apply the lockscreen visibility of the current state. - deviceEntryInteractor.get().isLockscreenEnabled() && - KeyguardState.lockscreenVisibleInState(currentState) - } else { - // If we're transitioning to GONE after a prior canceled transition from - // GONE, then this is the camera launch transition from an asleep state back - // to GONE. We don't want to show the lockscreen since we're aborting the - // lock and going back to GONE. + val transitionInfo = transitionRepository.currentTransitionInfoInternal.value + val wakingDirectlyToGone = + deviceIsAsleepInState(transitionInfo.from) && + transitionInfo.to == KeyguardState.GONE + + if (returningToGoneAfterCancellation || wakingDirectlyToGone) { + // GONE -> AOD/DOZING (cancel) -> GONE is the camera launch transition, + // which means we never want to show the lockscreen throughout the + // transition. Same for waking directly to gone, due to the lockscreen being + // disabled or because the device was woken back up before the lock timeout + // duration elapsed. KeyguardState.lockscreenVisibleInState(KeyguardState.GONE) + } else if (canWakeDirectlyToGone) { + // Never show the lockscreen if we can wake directly to GONE. This means + // that the lock timeout has not yet elapsed, or the keyguard is disabled. + // In either case, we don't show the activity lock screen until one of those + // conditions changes. + false + } else if ( + currentState == KeyguardState.DREAMING && + deviceEntryInteractor.get().isUnlocked.value + ) { + // Dreams dismiss keyguard and return to GONE if they can. + false + } else { + // Otherwise, use the visibility of the current state. + KeyguardState.lockscreenVisibleInState(currentState) } } .distinctUntilChanged() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 8f149fbe99de..f96f053b8da1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -37,6 +37,7 @@ import androidx.activity.setViewTreeOnBackPressedDispatcherOwner import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.animation.Interpolators +import com.android.app.tracing.coroutines.launch import com.android.internal.jank.InteractionJankMonitor import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD import com.android.systemui.Flags.newAodTransition @@ -80,6 +81,7 @@ import com.android.systemui.util.ui.isAnimating import com.android.systemui.util.ui.stopAnimating import com.android.systemui.util.ui.value import kotlin.math.min +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.coroutineScope @@ -110,6 +112,7 @@ object KeyguardRootViewBinder { vibratorHelper: VibratorHelper?, falsingManager: FalsingManager?, keyguardViewMediator: KeyguardViewMediator?, + mainImmediateDispatcher: CoroutineDispatcher, ): DisposableHandle { val disposables = DisposableHandles() val childViews = mutableMapOf<Int, View>() @@ -128,6 +131,30 @@ object KeyguardRootViewBinder { val burnInParams = MutableStateFlow(BurnInParameters()) val viewState = ViewStateAccessor(alpha = { view.alpha }) + + disposables += + view.repeatWhenAttached(mainImmediateDispatcher) { + repeatOnLifecycle(Lifecycle.State.CREATED) { + if (MigrateClocksToBlueprint.isEnabled) { + launch("$TAG#topClippingBounds") { + val clipBounds = Rect() + viewModel.topClippingBounds.collect { clipTop -> + if (clipTop == null) { + view.setClipBounds(null) + } else { + clipBounds.apply { + top = clipTop + left = view.getLeft() + right = view.getRight() + bottom = view.getBottom() + } + view.setClipBounds(clipBounds) + } + } + } + } + } + } disposables += view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { @@ -192,23 +219,6 @@ object KeyguardRootViewBinder { } launch { - val clipBounds = Rect() - viewModel.topClippingBounds.collect { clipTop -> - if (clipTop == null) { - view.setClipBounds(null) - } else { - clipBounds.apply { - top = clipTop - left = view.getLeft() - right = view.getRight() - bottom = view.getBottom() - } - view.setClipBounds(clipBounds) - } - } - } - - launch { viewModel.lockscreenStateAlpha(viewState).collect { alpha -> childViews[statusViewId]?.alpha = alpha } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index 4f0ac42a0e87..bc5b7b923082 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -394,6 +394,7 @@ constructor( null, // device entry haptics not required for preview mode null, // falsing manager not required for preview mode null, // keyguard view mediator is not required for preview mode + mainDispatcher, ) } rootView.addView( diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt index 6224170fd906..83f694bb1980 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt @@ -104,7 +104,7 @@ abstract class BaseMediaProjectionPermissionDialogDelegate<T : AlertDialog>( private fun initScreenShareSpinner() { val adapter = OptionsAdapter(dialog.context.applicationContext, screenShareOptions) - screenShareModeSpinner = dialog.requireViewById(R.id.screen_share_mode_spinner) + screenShareModeSpinner = dialog.requireViewById(R.id.screen_share_mode_options) screenShareModeSpinner.adapter = adapter screenShareModeSpinner.onItemSelectedListener = this diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt index 8858041ae529..9ce8070131fa 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt @@ -30,7 +30,7 @@ class MediaProjectionPermissionDialogDelegate( private val onStartRecordingClicked: Consumer<MediaProjectionPermissionDialogDelegate>, private val onCancelClicked: Runnable, private val appName: String?, - private val forceShowPartialScreenshare: Boolean, + forceShowPartialScreenshare: Boolean, hostUid: Int, mediaProjectionMetricsLogger: MediaProjectionMetricsLogger, ) : diff --git a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt index 7505566898c0..776a8f47f056 100644 --- a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt +++ b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt @@ -54,7 +54,10 @@ enum class WakeSleepReason( OTHER(isTouch = false, PowerManager.WAKE_REASON_UNKNOWN), /** Device goes to sleep due to folding of a foldable device. */ - FOLD(isTouch = false, PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD); + FOLD(isTouch = false, PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD), + + /** Device goes to sleep because it timed out. */ + TIMEOUT(isTouch = false, PowerManager.GO_TO_SLEEP_REASON_TIMEOUT); companion object { fun fromPowerManagerWakeReason(reason: Int): WakeSleepReason { @@ -75,6 +78,7 @@ enum class WakeSleepReason( fun fromPowerManagerSleepReason(reason: Int): WakeSleepReason { return when (reason) { PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON -> POWER_BUTTON + PowerManager.GO_TO_SLEEP_REASON_TIMEOUT -> TIMEOUT PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD -> FOLD else -> OTHER } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt index 0bb4cfa327a9..127ef8468111 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt @@ -22,6 +22,7 @@ import android.graphics.drawable.Animatable import android.service.quicksettings.Tile.STATE_ACTIVE import android.service.quicksettings.Tile.STATE_INACTIVE import android.text.TextUtils +import android.util.Log import androidx.appcompat.content.res.AppCompatResources import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi import androidx.compose.animation.graphics.res.animatedVectorResource @@ -81,7 +82,6 @@ import com.android.systemui.common.ui.compose.Icon import com.android.systemui.common.ui.compose.load import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel -import com.android.systemui.qs.panels.ui.viewmodel.TileUiState import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel import com.android.systemui.qs.panels.ui.viewmodel.toUiState import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor @@ -91,7 +91,6 @@ import com.android.systemui.res.R import java.util.function.Supplier import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.mapLatest object TileType @@ -103,29 +102,27 @@ fun Tile( showLabels: Boolean = false, modifier: Modifier, ) { - val state: TileUiState by - tile.state - .mapLatest { it.toUiState() } - .collectAsStateWithLifecycle(tile.currentState.toUiState()) - val colors = TileDefaults.getColorForState(state.state) + val state by tile.state.collectAsStateWithLifecycle(tile.currentState) + val uiState = remember(state) { state.toUiState() } + val colors = TileDefaults.getColorForState(uiState.state) TileContainer( colors = colors, showLabels = showLabels, - label = state.label.toString(), + label = uiState.label, iconOnly = iconOnly, clickEnabled = true, onClick = tile::onClick, onLongClick = tile::onLongClick, modifier = modifier, ) { - val icon = getTileIcon(icon = state.icon) + val icon = getTileIcon(icon = uiState.icon) if (iconOnly) { TileIcon(icon = icon, color = colors.icon, modifier = Modifier.align(Alignment.Center)) } else { LargeTileContent( - label = state.label.toString(), - secondaryLabel = state.secondaryLabel.toString(), + label = uiState.label, + secondaryLabel = uiState.secondaryLabel, icon = icon, colors = colors, clickEnabled = true, @@ -234,19 +231,26 @@ private fun LargeTileContent( Text( label, color = colors.label, - modifier = Modifier.basicMarquee(), + modifier = Modifier.tileMarquee(), ) if (!TextUtils.isEmpty(secondaryLabel)) { Text( secondaryLabel ?: "", color = colors.secondaryLabel, - modifier = Modifier.basicMarquee(), + modifier = Modifier.tileMarquee(), ) } } } } +private fun Modifier.tileMarquee(): Modifier { + return basicMarquee( + iterations = 1, + initialDelayMillis = 200, + ) +} + @Composable fun TileLazyGrid( modifier: Modifier = Modifier, @@ -452,6 +456,7 @@ private fun TileIcon( animateToEnd: Boolean = false, modifier: Modifier = Modifier, ) { + Log.d("Fabian", "Recomposing tile icon") val iconModifier = modifier.size(dimensionResource(id = R.dimen.qs_icon_size)) val context = LocalContext.current val loadedDrawable = diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt index b3acaced8ef2..bb004946a4d1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt @@ -26,8 +26,8 @@ import com.android.systemui.qs.pipeline.shared.TileSpec import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.mapLatest @@ -54,14 +54,24 @@ constructor( quickQuickSettingsRowInteractor.defaultRows ) - val tileViewModels: Flow<List<SizedTile<TileViewModel>>> = - columns.flatMapLatest { columns -> - tilesInteractor.currentTiles.combine(rows, ::Pair).mapLatest { (tiles, rows) -> - tiles - .map { SizedTile(TileViewModel(it.tile, it.spec), it.spec.width) } - .let { splitInRowsSequence(it, columns).take(rows).toList().flatten() } + val tileViewModels: StateFlow<List<SizedTile<TileViewModel>>> = + columns + .flatMapLatest { columns -> + tilesInteractor.currentTiles.combine(rows, ::Pair).mapLatest { (tiles, rows) -> + tiles + .map { SizedTile(TileViewModel(it.tile, it.spec), it.spec.width) } + .let { splitInRowsSequence(it, columns).take(rows).toList().flatten() } + } } - } + .stateIn( + applicationScope, + SharingStarted.WhileSubscribed(), + tilesInteractor.currentTiles.value + .map { SizedTile(TileViewModel(it.tile, it.spec), it.spec.width) } + .let { + splitInRowsSequence(it, columns.value).take(rows.value).toList().flatten() + } + ) private val TileSpec.width: Int get() = if (iconTilesViewModel.isIconTile(this)) 1 else 2 diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt index 578a292deb7c..4ec59c969a59 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt @@ -16,20 +16,22 @@ package com.android.systemui.qs.panels.ui.viewmodel +import androidx.compose.runtime.Immutable import com.android.systemui.plugins.qs.QSTile import java.util.function.Supplier +@Immutable data class TileUiState( - val label: CharSequence, - val secondaryLabel: CharSequence, + val label: String, + val secondaryLabel: String, val state: Int, val icon: Supplier<QSTile.Icon>, ) fun QSTile.State.toUiState(): TileUiState { return TileUiState( - label ?: "", - secondaryLabel ?: "", + label?.toString() ?: "", + secondaryLabel?.toString() ?: "", state, icon?.let { Supplier { icon } } ?: iconSupplier, ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt index 7505b90ee844..8578bb0ef9a1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.panels.ui.viewmodel +import androidx.compose.runtime.Immutable import com.android.systemui.animation.Expandable import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.pipeline.shared.TileSpec @@ -25,6 +26,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.onStart +@Immutable class TileViewModel(private val tile: QSTile, val spec: TileSpec) { val state: Flow<QSTile.State> = conflatedCallbackFlow { diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt index bbf4e51b35e9..8a51ad4cbd71 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt @@ -87,7 +87,10 @@ constructor( setNegativeButton(R.string.cancel) { _, _ -> } setPositiveButton(R.string.qs_record_issue_start) { _, _ -> onStarted.run() } } - bgExecutor.execute { traceurMessageSender.bindToTraceur(dialog.context) } + bgExecutor.execute { + traceurMessageSender.onBoundToTraceur.add { traceurMessageSender.getTags() } + traceurMessageSender.bindToTraceur(dialog.context) + } } override fun createDialog(): SystemUIDialog = factory.create(this) diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/TraceurMessageSender.kt b/packages/SystemUI/src/com/android/systemui/recordissue/TraceurMessageSender.kt index 903d662c69ff..a31a9ef26b16 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/TraceurMessageSender.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/TraceurMessageSender.kt @@ -45,11 +45,15 @@ class TraceurMessageSender @Inject constructor(@Background private val backgroun private var binder: Messenger? = null private var isBound: Boolean = false + val onBoundToTraceur = mutableListOf<Runnable>() + private val traceurConnection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, service: IBinder) { binder = Messenger(service) isBound = true + onBoundToTraceur.forEach(Runnable::run) + onBoundToTraceur.clear() } override fun onServiceDisconnected(className: ComponentName) { @@ -103,11 +107,17 @@ class TraceurMessageSender @Inject constructor(@Background private val backgroun @WorkerThread fun shareTraces(context: Context, screenRecord: Uri?) { - val replyHandler = Messenger(TraceurMessageHandler(context, screenRecord, backgroundLooper)) + val replyHandler = Messenger(ShareFilesHandler(context, screenRecord, backgroundLooper)) notifyTraceur(MessageConstants.SHARE_WHAT, replyTo = replyHandler) } @WorkerThread + fun getTags() { + val replyHandler = Messenger(TagsHandler(backgroundLooper)) + notifyTraceur(MessageConstants.TAGS_WHAT, replyTo = replyHandler) + } + + @WorkerThread private fun notifyTraceur(what: Int, data: Bundle = Bundle(), replyTo: Messenger? = null) { try { binder!!.send( @@ -122,7 +132,7 @@ class TraceurMessageSender @Inject constructor(@Background private val backgroun } } - private class TraceurMessageHandler( + private class ShareFilesHandler( private val context: Context, private val screenRecord: Uri?, looper: Looper, @@ -154,4 +164,29 @@ class TraceurMessageSender @Inject constructor(@Background private val backgroun context.startActivity(fileSharingIntent) } } + + private class TagsHandler(looper: Looper) : Handler(looper) { + + override fun handleMessage(msg: Message) { + if (MessageConstants.TAGS_WHAT == msg.what) { + val keys = msg.data.getStringArrayList(MessageConstants.BUNDLE_KEY_TAGS) + val values = + msg.data.getStringArrayList(MessageConstants.BUNDLE_KEY_TAG_DESCRIPTIONS) + if (keys == null || values == null) { + throw IllegalArgumentException( + "Neither keys: $keys, nor values: $values can " + "be null" + ) + } + + val tags = keys.zip(values).map { "${it.first}: ${it.second}" }.toSet() + Log.e( + TAG, + "These tags: $tags will be saved and used for the Custom Trace" + + " Config dialog in a future CL. This log will be removed." + ) + } else { + throw IllegalArgumentException("received unknown msg.what: " + msg.what) + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index c87b1f526957..95ee2e06817b 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -20,7 +20,6 @@ import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; import static com.android.systemui.Flags.screenshotPrivateProfileAccessibilityAnnouncementFix; -import static com.android.systemui.Flags.screenshotShelfUi2; import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM; import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK; import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT; @@ -407,22 +406,16 @@ public class ScreenshotController implements ScreenshotHandler { } final UUID requestId; - if (screenshotShelfUi2()) { - requestId = mActionsController.setCurrentScreenshot(screenshot); - saveScreenshotInBackground(screenshot, requestId, finisher); - - if (screenshot.getTaskId() >= 0) { - mAssistContentRequester.requestAssistContent( - screenshot.getTaskId(), - assistContent -> - mActionsController.onAssistContent(requestId, assistContent)); - } else { - mActionsController.onAssistContent(requestId, null); - } + requestId = mActionsController.setCurrentScreenshot(screenshot); + saveScreenshotInBackground(screenshot, requestId, finisher); + + if (screenshot.getTaskId() >= 0) { + mAssistContentRequester.requestAssistContent( + screenshot.getTaskId(), + assistContent -> + mActionsController.onAssistContent(requestId, assistContent)); } else { - requestId = UUID.randomUUID(); // passed through but unused for legacy UI - saveScreenshotInWorkerThread(screenshot.getUserHandle(), finisher, - this::showUiOnActionsReady, this::showUiOnQuickShareActionReady); + mActionsController.onAssistContent(requestId, null); } // The window is focusable by default @@ -458,9 +451,6 @@ public class ScreenshotController implements ScreenshotHandler { // ignore system bar insets for the purpose of window layout mWindow.getDecorView().setOnApplyWindowInsetsListener( (v, insets) -> WindowInsets.CONSUMED); - if (!screenshotShelfUi2()) { - mScreenshotHandler.cancelTimeout(); // restarted after animation - } } private boolean shouldShowUi() { @@ -515,11 +505,7 @@ public class ScreenshotController implements ScreenshotHandler { } boolean isPendingSharedTransition() { - if (screenshotShelfUi2()) { - return mActionExecutor.isPendingSharedTransition(); - } else { - return mViewProxy.isPendingSharedTransition(); - } + return mActionExecutor.isPendingSharedTransition(); } // Any cleanup needed when the service is being destroyed. @@ -603,11 +589,7 @@ public class ScreenshotController implements ScreenshotHandler { if (mConfigChanges.applyNewConfig(mContext.getResources())) { // Hide the scroll chip until we know it's available in this // orientation - if (screenshotShelfUi2()) { - mActionsController.onScrollChipInvalidated(); - } else { - mViewProxy.hideScrollChip(); - } + mActionsController.onScrollChipInvalidated(); // Delay scroll capture eval a bit to allow the underlying activity // to set up in the new orientation. mScreenshotHandler.postDelayed( @@ -640,13 +622,8 @@ public class ScreenshotController implements ScreenshotHandler { (response) -> { mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION, 0, response.getPackageName()); - if (screenshotShelfUi2()) { - mActionsController.onScrollChipReady(requestId, - () -> onScrollButtonClicked(owner, response)); - } else { - mViewProxy.showScrollChip(response.getPackageName(), - () -> onScrollButtonClicked(owner, response)); - } + mActionsController.onScrollChipReady(requestId, + () -> onScrollButtonClicked(owner, response)); return Unit.INSTANCE; } ); @@ -715,11 +692,9 @@ public class ScreenshotController implements ScreenshotHandler { mWindowManager.addView(decorView, mWindowLayoutParams); decorView.requestApplyInsets(); - if (screenshotShelfUi2()) { - ViewGroup layout = decorView.requireViewById(android.R.id.content); - layout.setClipChildren(false); - layout.setClipToPadding(false); - } + ViewGroup layout = decorView.requireViewById(android.R.id.content); + layout.setClipChildren(false); + layout.setClipToPadding(false); } void removeWindow() { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java index 8235325fffad..56ba1af4e83b 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java @@ -16,15 +16,12 @@ package com.android.systemui.screenshot.dagger; -import static com.android.systemui.Flags.screenshotShelfUi2; - import android.app.Service; import android.view.accessibility.AccessibilityManager; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.screenshot.ImageCapture; import com.android.systemui.screenshot.ImageCaptureImpl; -import com.android.systemui.screenshot.LegacyScreenshotViewProxy; import com.android.systemui.screenshot.ScreenshotPolicy; import com.android.systemui.screenshot.ScreenshotPolicyImpl; import com.android.systemui.screenshot.ScreenshotShelfViewProxy; @@ -96,14 +93,7 @@ public abstract class ScreenshotModule { return new ScreenshotViewModel(accessibilityManager); } - @Provides - static ScreenshotViewProxy.Factory providesScreenshotViewProxyFactory( - ScreenshotShelfViewProxy.Factory shelfScreenshotViewProxyFactory, - LegacyScreenshotViewProxy.Factory legacyScreenshotViewProxyFactory) { - if (screenshotShelfUi2()) { - return shelfScreenshotViewProxyFactory; - } else { - return legacyScreenshotViewProxyFactory; - } - } + @Binds + abstract ScreenshotViewProxy.Factory bindScreenshotViewProxyFactory( + ScreenshotShelfViewProxy.Factory shelfScreenshotViewProxyFactory); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt index 21f301c47467..6917f468ce14 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt @@ -49,7 +49,7 @@ constructor( activeCastDevice .map { if (it != null) { - MediaRouterCastModel.Casting + MediaRouterCastModel.Casting(deviceName = it.name) } else { MediaRouterCastModel.DoingNothing } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/model/MediaRouterCastModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/model/MediaRouterCastModel.kt index b228922d1721..1f84d7cb60b4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/model/MediaRouterCastModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/model/MediaRouterCastModel.kt @@ -21,6 +21,10 @@ sealed interface MediaRouterCastModel { /** MediaRouter isn't aware of any active cast. */ data object DoingNothing : MediaRouterCastModel - /** MediaRouter has an active cast. */ - data object Casting : MediaRouterCastModel + /** + * MediaRouter has an active cast. + * + * @property deviceName the name of the device receiving the cast. + */ + data class Casting(val deviceName: String?) : MediaRouterCastModel } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt index ffb20a7fd3b8..cac3f252a7e4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt @@ -16,7 +16,9 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.view +import android.content.Context import android.os.Bundle +import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.res.R import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.CastToOtherDeviceChipViewModel.Companion.CAST_TO_OTHER_DEVICE_ICON import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel @@ -26,6 +28,7 @@ import com.android.systemui.statusbar.phone.SystemUIDialog /** A dialog that lets the user stop an ongoing cast-screen-to-other-device event. */ class EndCastScreenToOtherDeviceDialogDelegate( private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, + private val context: Context, private val stopAction: () -> Unit, private val state: ProjectionChipModel.Projecting, ) : SystemUIDialog.Delegate { @@ -36,16 +39,8 @@ class EndCastScreenToOtherDeviceDialogDelegate( override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { with(dialog) { setIcon(CAST_TO_OTHER_DEVICE_ICON) - setTitle(R.string.cast_screen_to_other_device_stop_dialog_title) - // TODO(b/332662551): Include device name in this string. - setMessage( - endMediaProjectionDialogHelper.getDialogMessage( - state.projectionState, - genericMessageResId = R.string.cast_screen_to_other_device_stop_dialog_message, - specificAppMessageResId = - R.string.cast_screen_to_other_device_stop_dialog_message_specific_app, - ) - ) + setTitle(R.string.cast_to_other_device_stop_dialog_title) + setMessage(getMessage()) // No custom on-click, because the dialog will automatically be dismissed when the // button is clicked anyway. setNegativeButton(R.string.close_dialog_button, /* onClick= */ null) @@ -54,4 +49,41 @@ class EndCastScreenToOtherDeviceDialogDelegate( } } } + + private fun getMessage(): String { + return if (state.projectionState is MediaProjectionState.Projecting.SingleTask) { + val appBeingSharedName = + endMediaProjectionDialogHelper.getAppName(state.projectionState) + if (appBeingSharedName != null && state.deviceName != null) { + context.getString( + R.string.cast_to_other_device_stop_dialog_message_specific_app_with_device, + appBeingSharedName, + state.deviceName, + ) + } else if (appBeingSharedName != null) { + context.getString( + R.string.cast_to_other_device_stop_dialog_message_specific_app, + appBeingSharedName, + ) + } else if (state.deviceName != null) { + context.getString( + R.string.cast_to_other_device_stop_dialog_message_generic_with_device, + state.deviceName + ) + } else { + context.getString(R.string.cast_to_other_device_stop_dialog_message_generic) + } + } else { + if (state.deviceName != null) { + context.getString( + R.string.cast_to_other_device_stop_dialog_message_entire_screen_with_device, + state.deviceName + ) + } else { + context.getString( + R.string.cast_to_other_device_stop_dialog_message_entire_screen, + ) + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt index afe67b489abb..7dc9b255badc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.view +import android.content.Context import android.os.Bundle import com.android.systemui.res.R import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.CastToOtherDeviceChipViewModel.Companion.CAST_TO_OTHER_DEVICE_ICON @@ -29,6 +30,8 @@ import com.android.systemui.statusbar.phone.SystemUIDialog */ class EndGenericCastToOtherDeviceDialogDelegate( private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, + private val context: Context, + private val deviceName: String?, private val stopAction: () -> Unit, ) : SystemUIDialog.Delegate { override fun createDialog(): SystemUIDialog { @@ -36,11 +39,19 @@ class EndGenericCastToOtherDeviceDialogDelegate( } override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { + val message = + if (deviceName != null) { + context.getString( + R.string.cast_to_other_device_stop_dialog_message_generic_with_device, + deviceName, + ) + } else { + context.getString(R.string.cast_to_other_device_stop_dialog_message_generic) + } with(dialog) { setIcon(CAST_TO_OTHER_DEVICE_ICON) setTitle(R.string.cast_to_other_device_stop_dialog_title) - // TODO(b/332662551): Include device name in this string. - setMessage(R.string.cast_to_other_device_stop_dialog_message) + setMessage(message) // No custom on-click, because the dialog will automatically be dismissed when the // button is clicked anyway. setNegativeButton(R.string.close_dialog_button, /* onClick= */ null) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt index 2eff33610754..4183cdd4369d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel +import android.content.Context import androidx.annotation.DrawableRes import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription @@ -53,6 +54,7 @@ class CastToOtherDeviceChipViewModel @Inject constructor( @Application private val scope: CoroutineScope, + private val context: Context, private val mediaProjectionChipInteractor: MediaProjectionChipInteractor, private val mediaRouterChipInteractor: MediaRouterChipInteractor, private val systemClock: SystemClock, @@ -115,7 +117,7 @@ constructor( // This does mean that the audio-only casting chip will *never* show a // timer, because audio-only casting never activates the MediaProjection // APIs and those are the only cast APIs that show a timer. - createIconOnlyCastChip() + createIconOnlyCastChip(routerModel.deviceName) } } } @@ -178,7 +180,7 @@ constructor( ) } - private fun createIconOnlyCastChip(): OngoingActivityChipModel.Shown { + private fun createIconOnlyCastChip(deviceName: String?): OngoingActivityChipModel.Shown { return OngoingActivityChipModel.Shown.IconOnly( icon = Icon.Resource( @@ -188,7 +190,7 @@ constructor( ), colors = ColorsModel.Red, createDialogLaunchOnClickListener( - createGenericCastToOtherDeviceDialogDelegate(), + createGenericCastToOtherDeviceDialogDelegate(deviceName), dialogTransitionAnimator, ), ) @@ -199,13 +201,16 @@ constructor( ) = EndCastScreenToOtherDeviceDialogDelegate( endMediaProjectionDialogHelper, + context, stopAction = this::stopProjecting, state, ) - private fun createGenericCastToOtherDeviceDialogDelegate() = + private fun createGenericCastToOtherDeviceDialogDelegate(deviceName: String?) = EndGenericCastToOtherDeviceDialogDelegate( endMediaProjectionDialogHelper, + context, + deviceName, stopAction = this::stopMediaRouterCasting, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt index cda17cecbeff..ce60fab3ea6b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt @@ -74,7 +74,8 @@ constructor( }, { "State: Projecting(type=$str1 hostPackage=$str2)" } ) - ProjectionChipModel.Projecting(type, state) + // TODO(b/351851835): Get the device name. + ProjectionChipModel.Projecting(type, state, deviceName = null) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt index 85682f5eb8ff..a1a5e82e808e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt @@ -26,10 +26,16 @@ sealed class ProjectionChipModel { /** There is no media being projected. */ data object NotProjecting : ProjectionChipModel() - /** Media is currently being projected. */ + /** + * Media is currently being projected. + * + * @property deviceName the name of the device receiving the projection, or null if the + * projection is to this device (as opposed to a different device). + */ data class Projecting( val type: Type, val projectionState: MediaProjectionState.Projecting, + val deviceName: String?, ) : ProjectionChipModel() enum class Type { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt index 402306a12144..600436557efb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt @@ -16,13 +16,8 @@ package com.android.systemui.statusbar.chips.mediaprojection.ui.view -import android.annotation.StringRes import android.app.ActivityManager -import android.content.Context import android.content.pm.PackageManager -import android.text.Html -import android.text.Html.FROM_HTML_MODE_LEGACY -import android.text.TextUtils import com.android.systemui.dagger.SysUISingleton import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.statusbar.phone.SystemUIDialog @@ -35,63 +30,38 @@ class EndMediaProjectionDialogHelper constructor( private val dialogFactory: SystemUIDialog.Factory, private val packageManager: PackageManager, - private val context: Context ) { /** Creates a new [SystemUIDialog] using the given delegate. */ fun createDialog(delegate: SystemUIDialog.Delegate): SystemUIDialog { return dialogFactory.create(delegate) } - /** See other [getDialogMessage]. */ - fun getDialogMessage( - state: MediaProjectionState.Projecting, - @StringRes genericMessageResId: Int, - @StringRes specificAppMessageResId: Int, - ): CharSequence { + fun getAppName(state: MediaProjectionState.Projecting): CharSequence? { val specificTaskInfo = if (state is MediaProjectionState.Projecting.SingleTask) { state.task } else { null } - return getDialogMessage(specificTaskInfo, genericMessageResId, specificAppMessageResId) + return getAppName(specificTaskInfo) + } + + fun getAppName(specificTaskInfo: ActivityManager.RunningTaskInfo?): CharSequence? { + val packageName = specificTaskInfo?.baseIntent?.component?.packageName ?: return null + return getAppName(packageName) } /** - * Returns the message to show in the dialog based on the specific media projection state. - * - * @param genericMessageResId a res ID for a more generic "end projection" message - * @param specificAppMessageResId a res ID for an "end projection" message that also lets us - * specify which app is currently being projected. + * Returns the human-readable application name for the given package, or null if it couldn't be + * found for any reason. */ - fun getDialogMessage( - specificTaskInfo: ActivityManager.RunningTaskInfo?, - @StringRes genericMessageResId: Int, - @StringRes specificAppMessageResId: Int, - ): CharSequence { - if (specificTaskInfo == null) { - return context.getString(genericMessageResId) - } - val packageName = - specificTaskInfo.baseIntent.component?.packageName - ?: return context.getString(genericMessageResId) + fun getAppName(packageName: String): CharSequence? { return try { val appInfo = packageManager.getApplicationInfo(packageName, 0) - val appName = appInfo.loadLabel(packageManager) - getSpecificAppMessageText(specificAppMessageResId, appName) + appInfo.loadLabel(packageManager) } catch (e: PackageManager.NameNotFoundException) { // TODO(b/332662551): Log this error. - context.getString(genericMessageResId) + null } } - - private fun getSpecificAppMessageText( - @StringRes specificAppMessageResId: Int, - appName: CharSequence, - ): CharSequence { - // https://developer.android.com/guide/topics/resources/string-resource#StylingWithHTML - val escapedAppName = TextUtils.htmlEncode(appName.toString()) - val text = context.getString(specificAppMessageResId, escapedAppName) - return Html.fromHtml(text, FROM_HTML_MODE_LEGACY) - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt index 9adbff9d304a..1eca827d55c4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.chips.screenrecord.ui.view import android.app.ActivityManager +import android.content.Context import android.os.Bundle import com.android.systemui.res.R import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper @@ -26,6 +27,7 @@ import com.android.systemui.statusbar.phone.SystemUIDialog /** A dialog that lets the user stop an ongoing screen recording. */ class EndScreenRecordingDialogDelegate( private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, + val context: Context, private val stopAction: () -> Unit, private val recordedTask: ActivityManager.RunningTaskInfo?, ) : SystemUIDialog.Delegate { @@ -35,16 +37,18 @@ class EndScreenRecordingDialogDelegate( } override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { + val appName = endMediaProjectionDialogHelper.getAppName(recordedTask) + val message = + if (appName != null) { + context.getString(R.string.screenrecord_stop_dialog_message_specific_app, appName) + } else { + context.getString(R.string.screenrecord_stop_dialog_message) + } + with(dialog) { setIcon(ScreenRecordChipViewModel.ICON) setTitle(R.string.screenrecord_stop_dialog_title) - setMessage( - endMediaProjectionDialogHelper.getDialogMessage( - recordedTask, - genericMessageResId = R.string.screenrecord_stop_dialog_message, - specificAppMessageResId = R.string.screenrecord_stop_dialog_message_specific_app - ) - ) + setMessage(message) // No custom on-click, because the dialog will automatically be dismissed when the // button is clicked anyway. setNegativeButton(R.string.close_dialog_button, /* onClick= */ null) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt index 53679f1c0a6c..df25d57315ed 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel import android.app.ActivityManager +import android.content.Context import androidx.annotation.DrawableRes import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription @@ -47,6 +48,7 @@ class ScreenRecordChipViewModel @Inject constructor( @Application private val scope: CoroutineScope, + private val context: Context, private val interactor: ScreenRecordChipInteractor, private val systemClock: SystemClock, private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, @@ -90,6 +92,7 @@ constructor( ): EndScreenRecordingDialogDelegate { return EndScreenRecordingDialogDelegate( endMediaProjectionDialogHelper, + context, stopAction = interactor::stopRecording, recordedTask, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt index 7e7ef402b226..564f20e4b596 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt @@ -16,7 +16,9 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.view +import android.content.Context import android.os.Bundle +import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.res.R import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper @@ -26,6 +28,7 @@ import com.android.systemui.statusbar.phone.SystemUIDialog /** A dialog that lets the user stop an ongoing share-screen-to-app event. */ class EndShareToAppDialogDelegate( private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, + private val context: Context, private val stopAction: () -> Unit, private val state: ProjectionChipModel.Projecting, ) : SystemUIDialog.Delegate { @@ -37,13 +40,7 @@ class EndShareToAppDialogDelegate( with(dialog) { setIcon(SHARE_TO_APP_ICON) setTitle(R.string.share_to_app_stop_dialog_title) - setMessage( - endMediaProjectionDialogHelper.getDialogMessage( - state.projectionState, - genericMessageResId = R.string.share_to_app_stop_dialog_message, - specificAppMessageResId = R.string.share_to_app_stop_dialog_message_specific_app - ) - ) + setMessage(getMessage()) // No custom on-click, because the dialog will automatically be dismissed when the // button is clicked anyway. setNegativeButton(R.string.close_dialog_button, /* onClick= */ null) @@ -52,4 +49,32 @@ class EndShareToAppDialogDelegate( } } } + + private fun getMessage(): String { + return if (state.projectionState is MediaProjectionState.Projecting.SingleTask) { + // If a single app is being shared, use the name of the app being shared in the dialog + val appBeingSharedName = + endMediaProjectionDialogHelper.getAppName(state.projectionState) + if (appBeingSharedName != null) { + context.getString( + R.string.share_to_app_stop_dialog_message_single_app_specific, + appBeingSharedName, + ) + } else { + context.getString(R.string.share_to_app_stop_dialog_message_single_app_generic) + } + } else { + // Otherwise, use the name of the app *receiving* the share + val hostAppName = + endMediaProjectionDialogHelper.getAppName(state.projectionState.hostPackage) + if (hostAppName != null) { + context.getString( + R.string.share_to_app_stop_dialog_message_entire_screen_with_host_app, + hostAppName + ) + } else { + context.getString(R.string.share_to_app_stop_dialog_message_entire_screen) + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt index 8aef5a4e7629..c09772068093 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel +import android.content.Context import androidx.annotation.DrawableRes import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription @@ -48,6 +49,7 @@ class ShareToAppChipViewModel @Inject constructor( @Application private val scope: CoroutineScope, + private val context: Context, private val mediaProjectionChipInteractor: MediaProjectionChipInteractor, private val systemClock: SystemClock, private val dialogTransitionAnimator: DialogTransitionAnimator, @@ -97,6 +99,7 @@ constructor( private fun createShareToAppDialogDelegate(state: ProjectionChipModel.Projecting) = EndShareToAppDialogDelegate( endMediaProjectionDialogHelper, + context, stopAction = this::stopProjecting, state, ) diff --git a/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java b/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java index 64f8246118c8..a58a264c8348 100644 --- a/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java +++ b/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java @@ -27,6 +27,7 @@ import android.util.Log; import androidx.annotation.NonNull; +import com.android.app.tracing.TraceStateLogger; import com.android.systemui.Dumpable; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dump.DumpManager; @@ -41,11 +42,11 @@ import javax.inject.Named; /** * The {@link PersistentConnectionManager} is responsible for maintaining a connection to a * {@link ObservableServiceConnection}. + * * @param <T> The transformed connection type handled by the service. */ public class PersistentConnectionManager<T> implements Dumpable { private static final String TAG = "PersistentConnManager"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private final SystemClock mSystemClock; private final DelayableExecutor mBgExecutor; @@ -55,6 +56,7 @@ public class PersistentConnectionManager<T> implements Dumpable { private final Observer mObserver; private final DumpManager mDumpManager; private final String mDumpsysName; + private final TraceStateLogger mConnectionReasonLogger; private int mReconnectAttempts = 0; private Runnable mCurrentReconnectCancelable; @@ -64,36 +66,52 @@ public class PersistentConnectionManager<T> implements Dumpable { private final Runnable mConnectRunnable = new Runnable() { @Override public void run() { + mConnectionReasonLogger.log("ConnectionReasonRetry"); mCurrentReconnectCancelable = null; mConnection.bind(); } }; - private final Observer.Callback mObserverCallback = () -> initiateConnectionAttempt(); + private final Observer.Callback mObserverCallback = () -> initiateConnectionAttempt( + "ConnectionReasonObserver"); private final ObservableServiceConnection.Callback<T> mConnectionCallback = new ObservableServiceConnection.Callback<>() { - private long mStartTime; - - @Override - public void onConnected(ObservableServiceConnection connection, Object proxy) { - mStartTime = mSystemClock.currentTimeMillis(); - } - - @Override - public void onDisconnected(ObservableServiceConnection connection, int reason) { - // Do not attempt to reconnect if we were manually unbound - if (reason == ObservableServiceConnection.DISCONNECT_REASON_UNBIND) { - return; - } - - if (mSystemClock.currentTimeMillis() - mStartTime > mMinConnectionDuration) { - initiateConnectionAttempt(); - } else { - scheduleConnectionAttempt(); - } - } - }; + private long mStartTime = -1; + + @Override + public void onConnected(ObservableServiceConnection connection, Object proxy) { + mStartTime = mSystemClock.currentTimeMillis(); + } + + @Override + public void onDisconnected(ObservableServiceConnection connection, int reason) { + // Do not attempt to reconnect if we were manually unbound + if (reason == ObservableServiceConnection.DISCONNECT_REASON_UNBIND) { + return; + } + + if (mStartTime <= 0) { + Log.e(TAG, "onDisconnected called with invalid connection start time: " + + mStartTime); + return; + } + + final float connectionDuration = mSystemClock.currentTimeMillis() - mStartTime; + // Reset the start time. + mStartTime = -1; + + if (connectionDuration > mMinConnectionDuration) { + Log.i(TAG, "immediately reconnecting since service was connected for " + + connectionDuration + + "ms which is longer than the min duration of " + + mMinConnectionDuration + "ms"); + initiateConnectionAttempt("ConnectionReasonMinDurationMet"); + } else { + scheduleConnectionAttempt(); + } + } + }; @Inject public PersistentConnectionManager( @@ -112,6 +130,7 @@ public class PersistentConnectionManager<T> implements Dumpable { mObserver = observer; mDumpManager = dumpManager; mDumpsysName = TAG + "#" + dumpsysName; + mConnectionReasonLogger = new TraceStateLogger(mDumpsysName); mMaxReconnectAttempts = maxReconnectAttempts; mBaseReconnectDelayMs = baseReconnectDelayMs; @@ -125,7 +144,7 @@ public class PersistentConnectionManager<T> implements Dumpable { mDumpManager.registerCriticalDumpable(mDumpsysName, this); mConnection.addCallback(mConnectionCallback); mObserver.addCallback(mObserverCallback); - initiateConnectionAttempt(); + initiateConnectionAttempt("ConnectionReasonStart"); } /** @@ -140,6 +159,7 @@ public class PersistentConnectionManager<T> implements Dumpable { /** * Add a callback to the {@link ObservableServiceConnection}. + * * @param callback The callback to add. */ public void addConnectionCallback(ObservableServiceConnection.Callback<T> callback) { @@ -148,6 +168,7 @@ public class PersistentConnectionManager<T> implements Dumpable { /** * Remove a callback from the {@link ObservableServiceConnection}. + * * @param callback The callback to remove. */ public void removeConnectionCallback(ObservableServiceConnection.Callback<T> callback) { @@ -163,10 +184,10 @@ public class PersistentConnectionManager<T> implements Dumpable { mConnection.dump(pw); } - private void initiateConnectionAttempt() { + private void initiateConnectionAttempt(String reason) { + mConnectionReasonLogger.log(reason); // Reset attempts mReconnectAttempts = 0; - // The first attempt is always a direct invocation rather than delayed. mConnection.bind(); } @@ -179,20 +200,15 @@ public class PersistentConnectionManager<T> implements Dumpable { } if (mReconnectAttempts >= mMaxReconnectAttempts) { - if (DEBUG) { - Log.d(TAG, "exceeded max connection attempts."); - } + Log.d(TAG, "exceeded max connection attempts."); return; } final long reconnectDelayMs = (long) Math.scalb(mBaseReconnectDelayMs, mReconnectAttempts); - if (DEBUG) { - Log.d(TAG, - "scheduling connection attempt in " + reconnectDelayMs + "milliseconds"); - } - + Log.d(TAG, + "scheduling connection attempt in " + reconnectDelayMs + "milliseconds"); mCurrentReconnectCancelable = mBgExecutor.executeDelayed(mConnectRunnable, reconnectDelayMs); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt index 42ab25fcd9bd..032794c43f08 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt @@ -56,6 +56,7 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.Ignore import org.junit.runner.RunWith import org.mockito.Mockito.reset import org.mockito.Mockito.spy @@ -97,6 +98,7 @@ class FromDreamingTransitionInteractorTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + @Ignore("Until b/349837588 is fixed") fun testTransitionToOccluded_ifDreamEnds_occludingActivityOnTop() = testScope.runTest { kosmos.fakeKeyguardRepository.setDreaming(true) @@ -156,6 +158,7 @@ class FromDreamingTransitionInteractorTest : SysuiTestCase() { reset(transitionRepository) kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = false) + kosmos.fakeKeyguardRepository.setDreaming(false) runCurrent() assertThat(transitionRepository) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractorTest.kt index 459e41d7dbc0..ea5a41f6fd5c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractorTest.kt @@ -18,25 +18,22 @@ package com.android.systemui.keyguard.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.SysUITestModule import com.android.systemui.SysuiTestCase -import com.android.systemui.TestMocksModule -import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule import com.android.systemui.coroutines.collectValues -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository -import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.inWindowLauncherUnlockAnimationRepository +import com.android.systemui.keyguard.data.repository.keyguardSurfaceBehindRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.util.mockTopActivityClassName +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope import com.android.systemui.shared.system.ActivityManagerWrapper -import com.android.systemui.user.domain.UserDomainLayerModule -import dagger.BindsInstance -import dagger.Component +import com.android.systemui.shared.system.activityManagerWrapper +import com.android.systemui.testKosmos import junit.framework.Assert.assertEquals -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -49,10 +46,16 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) @kotlinx.coroutines.ExperimentalCoroutinesApi class InWindowLauncherUnlockAnimationInteractorTest : SysuiTestCase() { - private lateinit var underTest: InWindowLauncherUnlockAnimationInteractor - - private lateinit var testComponent: TestComponent - private lateinit var testScope: TestScope + private val kosmos = testKosmos() + private val underTest = + InWindowLauncherUnlockAnimationInteractor( + kosmos.inWindowLauncherUnlockAnimationRepository, + kosmos.applicationCoroutineScope, + kosmos.keyguardTransitionInteractor, + { kosmos.keyguardSurfaceBehindRepository }, + kosmos.activityManagerWrapper, + ) + private val testScope = kosmos.testScope private lateinit var transitionRepository: FakeKeyguardTransitionRepository @Mock private lateinit var activityManagerWrapper: ActivityManagerWrapper @@ -62,19 +65,9 @@ class InWindowLauncherUnlockAnimationInteractorTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - testComponent = - DaggerInWindowLauncherUnlockAnimationInteractorTest_TestComponent.factory() - .create( - test = this, - mocks = - TestMocksModule( - activityManagerWrapper = activityManagerWrapper, - ), - ) - underTest = testComponent.underTest - testScope = testComponent.testScope - transitionRepository = testComponent.transitionRepository + transitionRepository = kosmos.fakeKeyguardTransitionRepository + activityManagerWrapper = kosmos.activityManagerWrapper activityManagerWrapper.mockTopActivityClassName(launcherClassName) } @@ -92,7 +85,7 @@ class InWindowLauncherUnlockAnimationInteractorTest : SysuiTestCase() { ) // Put launcher on top - testComponent.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass( + kosmos.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass( launcherClassName ) activityManagerWrapper.mockTopActivityClassName(launcherClassName) @@ -175,7 +168,7 @@ class InWindowLauncherUnlockAnimationInteractorTest : SysuiTestCase() { ) // Put not launcher on top - testComponent.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass( + kosmos.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass( launcherClassName ) activityManagerWrapper.mockTopActivityClassName("not_launcher") @@ -252,7 +245,7 @@ class InWindowLauncherUnlockAnimationInteractorTest : SysuiTestCase() { ) // Put launcher on top - testComponent.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass( + kosmos.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass( launcherClassName ) activityManagerWrapper.mockTopActivityClassName(launcherClassName) @@ -296,7 +289,7 @@ class InWindowLauncherUnlockAnimationInteractorTest : SysuiTestCase() { ) // Put Launcher on top and begin transitioning to GONE. - testComponent.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass( + kosmos.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass( launcherClassName ) activityManagerWrapper.mockTopActivityClassName(launcherClassName) @@ -316,7 +309,7 @@ class InWindowLauncherUnlockAnimationInteractorTest : SysuiTestCase() { values ) - testComponent.surfaceBehindRepository.setSurfaceRemoteAnimationTargetAvailable(true) + kosmos.keyguardSurfaceBehindRepository.setSurfaceRemoteAnimationTargetAvailable(true) runCurrent() assertEquals( @@ -360,7 +353,7 @@ class InWindowLauncherUnlockAnimationInteractorTest : SysuiTestCase() { ) // Put Launcher on top and begin transitioning to GONE. - testComponent.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass( + kosmos.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass( launcherClassName ) activityManagerWrapper.mockTopActivityClassName(launcherClassName) @@ -402,7 +395,7 @@ class InWindowLauncherUnlockAnimationInteractorTest : SysuiTestCase() { ) // Put Launcher on top and begin transitioning to GONE. - testComponent.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass( + kosmos.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass( launcherClassName ) activityManagerWrapper.mockTopActivityClassName(launcherClassName) @@ -427,7 +420,7 @@ class InWindowLauncherUnlockAnimationInteractorTest : SysuiTestCase() { to = KeyguardState.AOD, ) ) - testComponent.surfaceBehindRepository.setSurfaceRemoteAnimationTargetAvailable(true) + kosmos.keyguardSurfaceBehindRepository.setSurfaceRemoteAnimationTargetAvailable(true) runCurrent() assertEquals( @@ -437,29 +430,4 @@ class InWindowLauncherUnlockAnimationInteractorTest : SysuiTestCase() { values ) } - - @SysUISingleton - @Component( - modules = - [ - SysUITestModule::class, - BiometricsDomainLayerModule::class, - UserDomainLayerModule::class, - ] - ) - interface TestComponent { - val underTest: InWindowLauncherUnlockAnimationInteractor - val testScope: TestScope - val transitionRepository: FakeKeyguardTransitionRepository - val surfaceBehindRepository: FakeKeyguardSurfaceBehindRepository - val inWindowLauncherUnlockAnimationRepository: InWindowLauncherUnlockAnimationRepository - - @Component.Factory - interface Factory { - fun create( - @BindsInstance test: SysuiTestCase, - mocks: TestMocksModule, - ): TestComponent - } - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt new file mode 100644 index 000000000000..22181f8fa568 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt @@ -0,0 +1,370 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.app.AlarmManager +import android.app.admin.alarmManager +import android.app.admin.devicePolicyManager +import android.content.BroadcastReceiver +import android.content.Intent +import android.content.mockedContext +import android.os.PowerManager +import android.os.UserHandle +import android.provider.Settings +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.widget.lockPatternUtils +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectValues +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.BiometricUnlockMode +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.kosmos.testScope +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.testKosmos +import com.android.systemui.util.settings.fakeSettings +import junit.framework.Assert.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class KeyguardWakeDirectlyToGoneInteractorTest : SysuiTestCase() { + + private var lastRegisteredBroadcastReceiver: BroadcastReceiver? = null + private val kosmos = + testKosmos().apply { + whenever(mockedContext.user).thenReturn(mock<UserHandle>()) + doAnswer { invocation -> + lastRegisteredBroadcastReceiver = invocation.arguments[0] as BroadcastReceiver + } + .whenever(mockedContext) + .registerReceiver(any(), any(), any(), any(), any()) + } + + private val testScope = kosmos.testScope + private val underTest = kosmos.keyguardWakeDirectlyToGoneInteractor + private val lockPatternUtils = kosmos.lockPatternUtils + private val repository = kosmos.fakeKeyguardRepository + private val transitionRepository = kosmos.fakeKeyguardTransitionRepository + + @Test + fun testCanWakeDirectlyToGone_keyguardServiceEnabledThenDisabled() = + testScope.runTest { + val canWake by collectValues(underTest.canWakeDirectlyToGone) + + assertEquals( + listOf( + false, // Defaults to false. + ), + canWake + ) + + repository.setKeyguardEnabled(false) + runCurrent() + + assertEquals( + listOf( + false, // Default to false. + true, // True now that keyguard service is disabled + ), + canWake + ) + + repository.setKeyguardEnabled(true) + runCurrent() + + assertEquals( + listOf( + false, + true, + false, + ), + canWake + ) + } + + @Test + fun testCanWakeDirectlyToGone_lockscreenDisabledThenEnabled() = + testScope.runTest { + val canWake by collectValues(underTest.canWakeDirectlyToGone) + + assertEquals( + listOf( + false, // Defaults to false. + ), + canWake + ) + + whenever(lockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(true) + runCurrent() + + assertEquals( + listOf( + // Still false - isLockScreenDisabled only causes canWakeDirectlyToGone to + // update on the next wake/sleep event. + false, + ), + canWake + ) + + kosmos.powerInteractor.setAsleepForTest() + runCurrent() + + assertEquals( + listOf( + false, + // True since we slept after setting isLockScreenDisabled=true + true, + ), + canWake + ) + + kosmos.powerInteractor.setAwakeForTest() + runCurrent() + + kosmos.powerInteractor.setAsleepForTest() + runCurrent() + + assertEquals( + listOf( + false, + true, + ), + canWake + ) + + whenever(lockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false) + kosmos.powerInteractor.setAwakeForTest() + runCurrent() + + assertEquals( + listOf( + false, + true, + false, + ), + canWake + ) + } + + @Test + fun testCanWakeDirectlyToGone_wakeAndUnlock() = + testScope.runTest { + val canWake by collectValues(underTest.canWakeDirectlyToGone) + + assertEquals( + listOf( + false, // Defaults to false. + ), + canWake + ) + + repository.setBiometricUnlockState(BiometricUnlockMode.WAKE_AND_UNLOCK) + runCurrent() + + assertEquals(listOf(false, true), canWake) + + repository.setBiometricUnlockState(BiometricUnlockMode.NONE) + runCurrent() + + assertEquals(listOf(false, true, false), canWake) + } + + @Test + fun testCanWakeDirectlyToGone_andSetsAlarm_ifPowerButtonDoesNotLockImmediately() = + testScope.runTest { + val canWake by collectValues(underTest.canWakeDirectlyToGone) + + assertEquals( + listOf( + false, // Defaults to false. + ), + canWake + ) + + repository.setCanIgnoreAuthAndReturnToGone(true) + runCurrent() + + assertEquals(listOf(false, true), canWake) + + repository.setCanIgnoreAuthAndReturnToGone(false) + runCurrent() + + assertEquals(listOf(false, true, false), canWake) + } + + @Test + fun testSetsCanIgnoreAuth_andSetsAlarm_whenTimingOut() = + testScope.runTest { + val canWake by collectValues(underTest.canWakeDirectlyToGone) + + assertEquals( + listOf( + false, // Defaults to false. + ), + canWake + ) + + whenever(kosmos.devicePolicyManager.getMaximumTimeToLock(eq(null), anyInt())) + .thenReturn(-1) + kosmos.fakeSettings.putInt(Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, 500) + + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + testScope, + ) + + kosmos.powerInteractor.setAsleepForTest( + sleepReason = PowerManager.GO_TO_SLEEP_REASON_TIMEOUT + ) + runCurrent() + + assertEquals( + listOf( + false, + true, + ), + canWake + ) + + verify(kosmos.alarmManager) + .setExactAndAllowWhileIdle( + eq(AlarmManager.ELAPSED_REALTIME_WAKEUP), + anyLong(), + any(), + ) + } + + @Test + fun testCancelsFirstAlarm_onWake_withSecondAlarmSet() = + testScope.runTest { + val canWake by collectValues(underTest.canWakeDirectlyToGone) + + assertEquals( + listOf( + false, // Defaults to false. + ), + canWake + ) + + whenever(kosmos.devicePolicyManager.getMaximumTimeToLock(eq(null), anyInt())) + .thenReturn(-1) + kosmos.fakeSettings.putInt(Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, 500) + + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + testScope, + ) + + kosmos.powerInteractor.setAsleepForTest( + sleepReason = PowerManager.GO_TO_SLEEP_REASON_TIMEOUT + ) + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + testScope = testScope, + ) + runCurrent() + + assertEquals( + listOf( + false, + // Timed out, so we can ignore auth/return to GONE. + true, + ), + canWake + ) + + verify(kosmos.alarmManager) + .setExactAndAllowWhileIdle( + eq(AlarmManager.ELAPSED_REALTIME_WAKEUP), + anyLong(), + any(), + ) + + kosmos.powerInteractor.setAwakeForTest() + transitionRepository.sendTransitionSteps( + from = KeyguardState.AOD, + to = KeyguardState.GONE, + testScope = testScope, + ) + runCurrent() + + assertEquals( + listOf( + false, + true, + // Should be canceled by the wakeup, but there would still be an + // alarm in flight that should be canceled. + false, + ), + canWake + ) + + kosmos.powerInteractor.setAsleepForTest( + sleepReason = PowerManager.GO_TO_SLEEP_REASON_TIMEOUT + ) + runCurrent() + + assertEquals( + listOf( + false, + true, + false, + // Back to sleep. + true, + ), + canWake + ) + + // Simulate the first sleep's alarm coming in. + lastRegisteredBroadcastReceiver?.onReceive( + kosmos.mockedContext, + Intent("com.android.internal.policy.impl.PhoneWindowManager.DELAYED_KEYGUARD") + ) + runCurrent() + + // It should not have any effect. + assertEquals( + listOf( + false, + true, + false, + true, + ), + canWake + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherUnlockAnimationManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherUnlockAnimationManagerTest.kt index c7f44164e582..0cfc20d7bbd8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherUnlockAnimationManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherUnlockAnimationManagerTest.kt @@ -18,17 +18,15 @@ package com.android.systemui.keyguard.ui.binder import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.SysUITestModule import com.android.systemui.SysuiTestCase -import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule -import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.inWindowLauncherUnlockAnimationInteractor import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager +import com.android.systemui.keyguard.ui.viewmodel.InWindowLauncherAnimationViewModel +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController -import com.android.systemui.user.domain.UserDomainLayerModule +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any -import dagger.BindsInstance -import dagger.Component -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -45,10 +43,9 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) @kotlinx.coroutines.ExperimentalCoroutinesApi class InWindowLauncherUnlockAnimationManagerTest : SysuiTestCase() { + private val kosmos = testKosmos() private lateinit var underTest: InWindowLauncherUnlockAnimationManager - - private lateinit var testComponent: TestComponent - private lateinit var testScope: TestScope + private val testScope = kosmos.testScope @Mock private lateinit var launcherUnlockAnimationController: ILauncherUnlockAnimationController @@ -56,14 +53,14 @@ class InWindowLauncherUnlockAnimationManagerTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - testComponent = - DaggerInWindowLauncherUnlockAnimationManagerTest_TestComponent.factory() - .create( - test = this, - ) - underTest = testComponent.underTest - testScope = testComponent.testScope - + underTest = + InWindowLauncherUnlockAnimationManager( + kosmos.inWindowLauncherUnlockAnimationInteractor, + InWindowLauncherAnimationViewModel( + kosmos.inWindowLauncherUnlockAnimationInteractor + ), + kosmos.applicationCoroutineScope + ) underTest.setLauncherUnlockController("launcherClass", launcherUnlockAnimationController) } @@ -114,25 +111,4 @@ class InWindowLauncherUnlockAnimationManagerTest : SysuiTestCase() { verifyNoMoreInteractions(launcherUnlockAnimationController) } - - @SysUISingleton - @Component( - modules = - [ - SysUITestModule::class, - BiometricsDomainLayerModule::class, - UserDomainLayerModule::class, - ] - ) - interface TestComponent { - val underTest: InWindowLauncherUnlockAnimationManager - val testScope: TestScope - - @Component.Factory - interface Factory { - fun create( - @BindsInstance test: SysuiTestCase, - ): TestComponent - } - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt index 548366e3fc38..d183c7370713 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt @@ -79,7 +79,7 @@ class MediaProjectionPermissionDialogDelegateTest : SysuiTestCase() { val overrideDisableSingleAppOption = false setUpAndShowDialog(overrideDisableSingleAppOption) - val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_spinner) + val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options) val secondOptionText = spinner.adapter .getDropDownView(1, null, spinner) @@ -100,7 +100,7 @@ class MediaProjectionPermissionDialogDelegateTest : SysuiTestCase() { val overrideDisableSingleAppOption = true setUpAndShowDialog(overrideDisableSingleAppOption) - val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_spinner) + val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options) val secondOptionText = spinner.adapter .getDropDownView(1, null, spinner) diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt index db607fd56643..cc8d7d532bda 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt @@ -109,7 +109,7 @@ class ScreenRecordPermissionDialogDelegateTest : SysuiTestCase() { fun testShowDialog_partialScreenSharingEnabled_optionsSpinnerIsVisible() { showDialog() - val visibility = dialog.requireViewById<Spinner>(R.id.screen_share_mode_spinner).visibility + val visibility = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options).visibility assertThat(visibility).isEqualTo(View.VISIBLE) } @@ -155,7 +155,7 @@ class ScreenRecordPermissionDialogDelegateTest : SysuiTestCase() { fun showDialog_singleAppIsDefault() { showDialog() - val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_spinner) + val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options) val singleApp = context.getString(R.string.screen_share_permission_dialog_option_single_app) assertEquals(spinner.adapter.getItem(0), singleApp) } @@ -217,7 +217,7 @@ class ScreenRecordPermissionDialogDelegateTest : SysuiTestCase() { } private fun onSpinnerItemSelected(position: Int) { - val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_spinner) + val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options) checkNotNull(spinner.onItemSelectedListener) .onItemSelected(spinner, mock(), position, /* id= */ 0) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt index 69e8f4737a5a..9e6a498b325a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt @@ -6,26 +6,23 @@ import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.ExpandHelper -import com.android.systemui.SysUITestModule import com.android.systemui.SysuiTestCase -import com.android.systemui.TestMocksModule -import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule import com.android.systemui.classifier.FalsingCollectorFake import com.android.systemui.classifier.FalsingManagerFake -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.flags.FakeFeatureFlagsClassicModule import com.android.systemui.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver +import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager import com.android.systemui.plugins.qs.QS -import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter import com.android.systemui.res.R +import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor -import com.android.systemui.shade.data.repository.FakeShadeRepository -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel -import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository +import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository +import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.NotificationTestHelper import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout @@ -33,17 +30,16 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll import com.android.systemui.statusbar.phone.CentralSurfaces import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.phone.ScrimController -import com.android.systemui.statusbar.policy.FakeConfigurationController import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController -import com.android.systemui.user.domain.UserDomainLayerModule +import com.android.systemui.statusbar.policy.configurationController +import com.android.systemui.statusbar.policy.fakeConfigurationController +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock -import dagger.BindsInstance -import dagger.Component import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull @@ -65,8 +61,8 @@ import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.verifyZeroInteractions -import org.mockito.junit.MockitoJUnit import org.mockito.Mockito.`when` as whenever +import org.mockito.junit.MockitoJUnit private fun <T> anyObject(): T { return Mockito.anyObject<T>() @@ -77,15 +73,14 @@ private fun <T> anyObject(): T { @RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) class LockscreenShadeTransitionControllerTest : SysuiTestCase() { - + private val kosmos = + testKosmos().apply { + fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) } + } private lateinit var transitionController: LockscreenShadeTransitionController - private lateinit var testComponent: TestComponent - private val configurationController - get() = testComponent.configurationController - private val disableFlagsRepository - get() = testComponent.disableFlagsRepository - private val testScope - get() = testComponent.testScope + private val configurationController = kosmos.fakeConfigurationController + private val disableFlagsRepository = kosmos.fakeDisableFlagsRepository + private val testScope = kosmos.testScope private val qsSceneAdapter = FakeQSSceneAdapter({ mock() }) @@ -134,26 +129,6 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { whenever(keyguardBypassController.bypassEnabled).thenReturn(false) whenever(naturalScrollingSettingObserver.isNaturalScrollingEnabled).thenReturn(true) - testComponent = - DaggerLockscreenShadeTransitionControllerTest_TestComponent.factory() - .create( - test = this, - featureFlags = - FakeFeatureFlagsClassicModule { - set(Flags.FULL_SCREEN_USER_SWITCHER, false) - }, - mocks = - TestMocksModule( - notificationShadeDepthController = depthController, - keyguardBypassController = keyguardBypassController, - mediaHierarchyManager = mediaHierarchyManager, - notificationLockscreenUserManager = lockScreenUserManager, - notificationStackScrollLayoutController = nsslController, - scrimController = scrimController, - statusBarStateController = statusbarStateController, - ) - ) - transitionController = LockscreenShadeTransitionController( statusBarStateController = statusbarStateController, @@ -191,10 +166,10 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { falsingManager = FalsingManagerFake(), dumpManager = mock(), qsTransitionControllerFactory = { qsTransitionController }, - shadeRepository = testComponent.shadeRepository, - shadeInteractor = testComponent.shadeInteractor, + shadeRepository = kosmos.shadeRepository, + shadeInteractor = kosmos.shadeInteractor, splitShadeStateController = ResourcesSplitShadeStateController(), - shadeLockscreenInteractorLazy = {shadeLockscreenInteractor}, + shadeLockscreenInteractorLazy = { shadeLockscreenInteractor }, naturalScrollingSettingObserver = naturalScrollingSettingObserver, lazyQSSceneAdapter = { qsSceneAdapter } ) @@ -214,387 +189,424 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { } @Test - fun testCantDragDownWhenQSExpanded() { - assertTrue("Can't drag down on keyguard", transitionController.canDragDown()) - whenever(qS.isFullyCollapsed).thenReturn(false) - assertFalse("Can drag down when QS is expanded", transitionController.canDragDown()) - } + fun testCantDragDownWhenQSExpanded() = + testScope.runTest { + assertTrue("Can't drag down on keyguard", transitionController.canDragDown()) + whenever(qS.isFullyCollapsed).thenReturn(false) + assertFalse("Can drag down when QS is expanded", transitionController.canDragDown()) + } @Test - fun testCanDragDownInLockedDownShade() { - whenever(statusbarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED) - assertFalse("Can drag down in shade locked", transitionController.canDragDown()) - whenever(nsslController.isInLockedDownShade).thenReturn(true) - assertTrue("Can't drag down in locked down shade", transitionController.canDragDown()) - } + fun testCanDragDownInLockedDownShade() = + testScope.runTest { + whenever(statusbarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED) + assertFalse("Can drag down in shade locked", transitionController.canDragDown()) + whenever(nsslController.isInLockedDownShade).thenReturn(true) + assertTrue("Can't drag down in locked down shade", transitionController.canDragDown()) + } @Test - fun testGoingToLockedShade() { - transitionController.goToLockedShade(null) - verify(statusbarStateController).setState(StatusBarState.SHADE_LOCKED) - } + fun testGoingToLockedShade() = + testScope.runTest { + transitionController.goToLockedShade(null) + verify(statusbarStateController).setState(StatusBarState.SHADE_LOCKED) + } @Test - fun testWakingToShadeLockedWhenDozing() { - whenever(statusbarStateController.isDozing).thenReturn(true) - transitionController.goToLockedShade(null) - verify(statusbarStateController).setState(StatusBarState.SHADE_LOCKED) - assertTrue("Not waking to shade locked", transitionController.isWakingToShadeLocked) - } + fun testWakingToShadeLockedWhenDozing() = + testScope.runTest { + whenever(statusbarStateController.isDozing).thenReturn(true) + transitionController.goToLockedShade(null) + verify(statusbarStateController).setState(StatusBarState.SHADE_LOCKED) + assertTrue("Not waking to shade locked", transitionController.isWakingToShadeLocked) + } @Test - fun testNotWakingToShadeLockedWhenNotDozing() { - whenever(statusbarStateController.isDozing).thenReturn(false) - transitionController.goToLockedShade(null) - verify(statusbarStateController).setState(StatusBarState.SHADE_LOCKED) - assertFalse( - "Waking to shade locked when not dozing", - transitionController.isWakingToShadeLocked - ) - } + fun testNotWakingToShadeLockedWhenNotDozing() = + testScope.runTest { + whenever(statusbarStateController.isDozing).thenReturn(false) + transitionController.goToLockedShade(null) + verify(statusbarStateController).setState(StatusBarState.SHADE_LOCKED) + assertFalse( + "Waking to shade locked when not dozing", + transitionController.isWakingToShadeLocked + ) + } @Test - fun testGoToLockedShadeOnlyOnKeyguard() { - whenever(statusbarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED) - transitionController.goToLockedShade(null) - whenever(statusbarStateController.state).thenReturn(StatusBarState.SHADE) - transitionController.goToLockedShade(null) - verify(statusbarStateController, never()).setState(anyInt()) - } + fun testGoToLockedShadeOnlyOnKeyguard() = + testScope.runTest { + whenever(statusbarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED) + transitionController.goToLockedShade(null) + whenever(statusbarStateController.state).thenReturn(StatusBarState.SHADE) + transitionController.goToLockedShade(null) + verify(statusbarStateController, never()).setState(anyInt()) + } @Test - fun testDontGoWhenShadeDisabled() { - disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_NOTIFICATION_SHADE, - ) - testScope.runCurrent() - transitionController.goToLockedShade(null) - verify(statusbarStateController, never()).setState(anyInt()) - } + fun testDontGoWhenShadeDisabled() = + testScope.runTest { + disableFlagsRepository.disableFlags.value = + DisableFlagsModel( + disable2 = DISABLE2_NOTIFICATION_SHADE, + ) + testScope.runCurrent() + transitionController.goToLockedShade(null) + verify(statusbarStateController, never()).setState(anyInt()) + } @Test - fun testUserExpandsViewOnGoingToFullShade() { - assertFalse("Row shouldn't be user expanded yet", row.isUserExpanded) - transitionController.goToLockedShade(row) - assertTrue("Row wasn't user expanded on drag down", row.isUserExpanded) - } + fun testUserExpandsViewOnGoingToFullShade() = + testScope.runTest { + assertFalse("Row shouldn't be user expanded yet", row.isUserExpanded) + transitionController.goToLockedShade(row) + assertTrue("Row wasn't user expanded on drag down", row.isUserExpanded) + } @Test - fun testTriggeringBouncerNoNotificationsOnLockscreen() { - whenever(lockScreenUserManager.shouldShowLockscreenNotifications()).thenReturn(false) - transitionController.goToLockedShade(null) - verify(statusbarStateController, never()).setState(anyInt()) - verify(statusbarStateController).setLeaveOpenOnKeyguardHide(true) - verify(centralSurfaces).showBouncerWithDimissAndCancelIfKeyguard(anyObject(), anyObject()) - } + fun testTriggeringBouncerNoNotificationsOnLockscreen() = + testScope.runTest { + whenever(lockScreenUserManager.shouldShowLockscreenNotifications()).thenReturn(false) + transitionController.goToLockedShade(null) + verify(statusbarStateController, never()).setState(anyInt()) + verify(statusbarStateController).setLeaveOpenOnKeyguardHide(true) + verify(centralSurfaces) + .showBouncerWithDimissAndCancelIfKeyguard(anyObject(), anyObject()) + } @Test - fun testGoToLockedShadeCreatesQSAnimation() { - transitionController.goToLockedShade(null) - verify(statusbarStateController).setState(StatusBarState.SHADE_LOCKED) - verify(shadeLockscreenInteractor).transitionToExpandedShade(anyLong()) - assertNotNull(transitionController.dragDownAnimator) - } + fun testGoToLockedShadeCreatesQSAnimation() = + testScope.runTest { + transitionController.goToLockedShade(null) + verify(statusbarStateController).setState(StatusBarState.SHADE_LOCKED) + verify(shadeLockscreenInteractor).transitionToExpandedShade(anyLong()) + assertNotNull(transitionController.dragDownAnimator) + } @Test - fun testGoToLockedShadeDoesntCreateQSAnimation() { - transitionController.goToLockedShade(null, needsQSAnimation = false) - verify(statusbarStateController).setState(StatusBarState.SHADE_LOCKED) - verify(shadeLockscreenInteractor).transitionToExpandedShade(anyLong()) - assertNull(transitionController.dragDownAnimator) - } + fun testGoToLockedShadeDoesntCreateQSAnimation() = + testScope.runTest { + transitionController.goToLockedShade(null, needsQSAnimation = false) + verify(statusbarStateController).setState(StatusBarState.SHADE_LOCKED) + verify(shadeLockscreenInteractor).transitionToExpandedShade(anyLong()) + assertNull(transitionController.dragDownAnimator) + } @Test - fun testGoToLockedShadeAlwaysCreatesQSAnimationInSplitShade() { - enableSplitShade() - transitionController.goToLockedShade(null, needsQSAnimation = true) - verify(shadeLockscreenInteractor).transitionToExpandedShade(anyLong()) - assertNotNull(transitionController.dragDownAnimator) - } + fun testGoToLockedShadeAlwaysCreatesQSAnimationInSplitShade() = + testScope.runTest { + enableSplitShade() + transitionController.goToLockedShade(null, needsQSAnimation = true) + verify(shadeLockscreenInteractor).transitionToExpandedShade(anyLong()) + assertNotNull(transitionController.dragDownAnimator) + } @Test - fun testGoToLockedShadeCancelDoesntLeaveShadeOpenOnKeyguardHide() { - whenever(lockScreenUserManager.shouldShowLockscreenNotifications()).thenReturn(false) - whenever(lockScreenUserManager.isLockscreenPublicMode(any())).thenReturn(true) - transitionController.goToLockedShade(null) - val captor = argumentCaptor<Runnable>() - verify(centralSurfaces).showBouncerWithDimissAndCancelIfKeyguard(isNull(), captor.capture()) - captor.value.run() - verify(statusbarStateController).setLeaveOpenOnKeyguardHide(false) - } + fun testGoToLockedShadeCancelDoesntLeaveShadeOpenOnKeyguardHide() = + testScope.runTest { + whenever(lockScreenUserManager.shouldShowLockscreenNotifications()).thenReturn(false) + whenever(lockScreenUserManager.isLockscreenPublicMode(any())).thenReturn(true) + transitionController.goToLockedShade(null) + val captor = argumentCaptor<Runnable>() + verify(centralSurfaces) + .showBouncerWithDimissAndCancelIfKeyguard(isNull(), captor.capture()) + captor.value.run() + verify(statusbarStateController).setLeaveOpenOnKeyguardHide(false) + } @Test - fun testDragDownAmountDoesntCallOutInLockedDownShade() { - whenever(nsslController.isInLockedDownShade).thenReturn(true) - transitionController.dragDownAmount = 10f - verify(nsslController, never()).setTransitionToFullShadeAmount(anyFloat()) - verify(mediaHierarchyManager, never()).setTransitionToFullShadeAmount(anyFloat()) - verify(scrimController, never()).setTransitionToFullShadeProgress(anyFloat(), anyFloat()) - verify(transitionControllerCallback, never()) - .setTransitionToFullShadeAmount(anyFloat(), anyBoolean(), anyLong()) - verify(qsTransitionController, never()).dragDownAmount = anyFloat() - } + fun testDragDownAmountDoesntCallOutInLockedDownShade() = + testScope.runTest { + whenever(nsslController.isInLockedDownShade).thenReturn(true) + transitionController.dragDownAmount = 10f + verify(nsslController, never()).setTransitionToFullShadeAmount(anyFloat()) + verify(mediaHierarchyManager, never()).setTransitionToFullShadeAmount(anyFloat()) + verify(scrimController, never()) + .setTransitionToFullShadeProgress(anyFloat(), anyFloat()) + verify(transitionControllerCallback, never()) + .setTransitionToFullShadeAmount(anyFloat(), anyBoolean(), anyLong()) + verify(qsTransitionController, never()).dragDownAmount = anyFloat() + } @Test - fun testDragDownAmountCallsOut() { - transitionController.dragDownAmount = 10f - verify(nsslController).setTransitionToFullShadeAmount(anyFloat()) - verify(mediaHierarchyManager).setTransitionToFullShadeAmount(anyFloat()) - verify(scrimController).setTransitionToFullShadeProgress(anyFloat(), anyFloat()) - verify(transitionControllerCallback) - .setTransitionToFullShadeAmount(anyFloat(), anyBoolean(), anyLong()) - verify(qsTransitionController).dragDownAmount = 10f - verify(depthController).transitionToFullShadeProgress = anyFloat() - } + fun testDragDownAmountCallsOut() = + testScope.runTest { + transitionController.dragDownAmount = 10f + verify(nsslController).setTransitionToFullShadeAmount(anyFloat()) + verify(mediaHierarchyManager).setTransitionToFullShadeAmount(anyFloat()) + verify(scrimController).setTransitionToFullShadeProgress(anyFloat(), anyFloat()) + verify(transitionControllerCallback) + .setTransitionToFullShadeAmount(anyFloat(), anyBoolean(), anyLong()) + verify(qsTransitionController).dragDownAmount = 10f + verify(depthController).transitionToFullShadeProgress = anyFloat() + } @Test - fun testDragDownAmount_depthDistanceIsZero_setsProgressToZero() { - context - .getOrCreateTestableResources() - .addOverride(R.dimen.lockscreen_shade_depth_controller_transition_distance, 0) - configurationController.notifyConfigurationChanged() + fun testDragDownAmount_depthDistanceIsZero_setsProgressToZero() = + testScope.runTest { + context + .getOrCreateTestableResources() + .addOverride(R.dimen.lockscreen_shade_depth_controller_transition_distance, 0) + configurationController.notifyConfigurationChanged() - transitionController.dragDownAmount = 10f + transitionController.dragDownAmount = 10f - verify(depthController).transitionToFullShadeProgress = 0f - } + verify(depthController).transitionToFullShadeProgress = 0f + } @Test - fun testDragDownAmount_depthDistanceNonZero_setsProgressBasedOnDistance() { - context - .getOrCreateTestableResources() - .addOverride(R.dimen.lockscreen_shade_depth_controller_transition_distance, 100) - configurationController.notifyConfigurationChanged() + fun testDragDownAmount_depthDistanceNonZero_setsProgressBasedOnDistance() = + testScope.runTest { + context + .getOrCreateTestableResources() + .addOverride(R.dimen.lockscreen_shade_depth_controller_transition_distance, 100) + configurationController.notifyConfigurationChanged() - transitionController.dragDownAmount = 10f + transitionController.dragDownAmount = 10f - verify(depthController).transitionToFullShadeProgress = 0.1f - } + verify(depthController).transitionToFullShadeProgress = 0.1f + } @Test - fun setDragAmount_setsKeyguardTransitionProgress() { - transitionController.dragDownAmount = 10f + fun setDragAmount_setsKeyguardTransitionProgress() = + testScope.runTest { + transitionController.dragDownAmount = 10f - verify(shadeLockscreenInteractor).setKeyguardTransitionProgress(anyFloat(), anyInt()) - } + verify(shadeLockscreenInteractor).setKeyguardTransitionProgress(anyFloat(), anyInt()) + } @Test - fun setDragAmount_setsKeyguardAlphaBasedOnDistance() { - val alphaDistance = - context.resources.getDimensionPixelSize( - R.dimen.lockscreen_shade_npvc_keyguard_content_alpha_transition_distance - ) - transitionController.dragDownAmount = 10f + fun setDragAmount_setsKeyguardAlphaBasedOnDistance() = + testScope.runTest { + val alphaDistance = + context.resources.getDimensionPixelSize( + R.dimen.lockscreen_shade_npvc_keyguard_content_alpha_transition_distance + ) + transitionController.dragDownAmount = 10f - val expectedAlpha = 1 - 10f / alphaDistance - verify(shadeLockscreenInteractor).setKeyguardTransitionProgress(eq(expectedAlpha), anyInt()) - } + val expectedAlpha = 1 - 10f / alphaDistance + verify(shadeLockscreenInteractor) + .setKeyguardTransitionProgress(eq(expectedAlpha), anyInt()) + } @Test - fun setDragAmount_notInSplitShade_setsKeyguardTranslationToZero() { - val mediaTranslationY = 123 - disableSplitShade() - whenever(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).thenReturn(true) - whenever(mediaHierarchyManager.getGuidedTransformationTranslationY()) - .thenReturn(mediaTranslationY) + fun setDragAmount_notInSplitShade_setsKeyguardTranslationToZero() = + testScope.runTest { + val mediaTranslationY = 123 + disableSplitShade() + whenever(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).thenReturn(true) + whenever(mediaHierarchyManager.getGuidedTransformationTranslationY()) + .thenReturn(mediaTranslationY) - transitionController.dragDownAmount = 10f + transitionController.dragDownAmount = 10f - verify(shadeLockscreenInteractor).setKeyguardTransitionProgress(anyFloat(), eq(0)) - } + verify(shadeLockscreenInteractor).setKeyguardTransitionProgress(anyFloat(), eq(0)) + } @Test - fun setDragAmount_inSplitShade_setsKeyguardTranslationBasedOnMediaTranslation() { - val mediaTranslationY = 123 - enableSplitShade() - whenever(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).thenReturn(true) - whenever(mediaHierarchyManager.getGuidedTransformationTranslationY()) - .thenReturn(mediaTranslationY) + fun setDragAmount_inSplitShade_setsKeyguardTranslationBasedOnMediaTranslation() = + testScope.runTest { + val mediaTranslationY = 123 + enableSplitShade() + whenever(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).thenReturn(true) + whenever(mediaHierarchyManager.getGuidedTransformationTranslationY()) + .thenReturn(mediaTranslationY) - transitionController.dragDownAmount = 10f + transitionController.dragDownAmount = 10f - verify(shadeLockscreenInteractor) + verify(shadeLockscreenInteractor) .setKeyguardTransitionProgress(anyFloat(), eq(mediaTranslationY)) - } + } @Test - fun setDragAmount_inSplitShade_mediaNotShowing_setsKeyguardTranslationBasedOnDistance() { - enableSplitShade() - whenever(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).thenReturn(false) - whenever(mediaHierarchyManager.getGuidedTransformationTranslationY()).thenReturn(123) + fun setDragAmount_inSplitShade_mediaNotShowing_setsKeyguardTranslationBasedOnDistance() = + testScope.runTest { + enableSplitShade() + whenever(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).thenReturn(false) + whenever(mediaHierarchyManager.getGuidedTransformationTranslationY()).thenReturn(123) - transitionController.dragDownAmount = 10f + transitionController.dragDownAmount = 10f - val distance = - context.resources.getDimensionPixelSize( - R.dimen.lockscreen_shade_keyguard_transition_distance - ) - val offset = - context.resources.getDimensionPixelSize( - R.dimen.lockscreen_shade_keyguard_transition_vertical_offset - ) - val expectedTranslation = 10f / distance * offset - verify(shadeLockscreenInteractor) - .setKeyguardTransitionProgress(anyFloat(), eq(expectedTranslation.toInt())) - } + val distance = + context.resources.getDimensionPixelSize( + R.dimen.lockscreen_shade_keyguard_transition_distance + ) + val offset = + context.resources.getDimensionPixelSize( + R.dimen.lockscreen_shade_keyguard_transition_vertical_offset + ) + val expectedTranslation = 10f / distance * offset + verify(shadeLockscreenInteractor) + .setKeyguardTransitionProgress(anyFloat(), eq(expectedTranslation.toInt())) + } @Test - fun setDragDownAmount_setsValueOnMediaHierarchyManager() { - transitionController.dragDownAmount = 10f + fun setDragDownAmount_setsValueOnMediaHierarchyManager() = + testScope.runTest { + transitionController.dragDownAmount = 10f - verify(mediaHierarchyManager).setTransitionToFullShadeAmount(10f) - } + verify(mediaHierarchyManager).setTransitionToFullShadeAmount(10f) + } @Test - fun setDragAmount_setsScrimProgressBasedOnScrimDistance() { - val distance = 10 - context.orCreateTestableResources.addOverride( - R.dimen.lockscreen_shade_scrim_transition_distance, - distance - ) - configurationController.notifyConfigurationChanged() + fun setDragAmount_setsScrimProgressBasedOnScrimDistance() = + testScope.runTest { + val distance = 10 + context.orCreateTestableResources.addOverride( + R.dimen.lockscreen_shade_scrim_transition_distance, + distance + ) + configurationController.notifyConfigurationChanged() - transitionController.dragDownAmount = 5f + transitionController.dragDownAmount = 5f - verify(scrimController) - .transitionToFullShadeProgress( - progress = eq(0.5f), - lockScreenNotificationsProgress = anyFloat() - ) - } + verify(scrimController) + .transitionToFullShadeProgress( + progress = eq(0.5f), + lockScreenNotificationsProgress = anyFloat() + ) + } @Test - fun setDragAmount_setsNotificationsScrimProgressBasedOnNotificationsScrimDistanceAndDelay() { - val distance = 100 - val delay = 10 - context.orCreateTestableResources.addOverride( - R.dimen.lockscreen_shade_notifications_scrim_transition_distance, - distance - ) - context.orCreateTestableResources.addOverride( - R.dimen.lockscreen_shade_notifications_scrim_transition_delay, - delay - ) - configurationController.notifyConfigurationChanged() + fun setDragAmount_setsNotificationsScrimProgressBasedOnNotificationsScrimDistanceAndDelay() = + testScope.runTest { + val distance = 100 + val delay = 10 + context.orCreateTestableResources.addOverride( + R.dimen.lockscreen_shade_notifications_scrim_transition_distance, + distance + ) + context.orCreateTestableResources.addOverride( + R.dimen.lockscreen_shade_notifications_scrim_transition_delay, + delay + ) + configurationController.notifyConfigurationChanged() - transitionController.dragDownAmount = 20f + transitionController.dragDownAmount = 20f - verify(scrimController) - .transitionToFullShadeProgress( - progress = anyFloat(), - lockScreenNotificationsProgress = eq(0.1f) - ) - } + verify(scrimController) + .transitionToFullShadeProgress( + progress = anyFloat(), + lockScreenNotificationsProgress = eq(0.1f) + ) + } @Test - fun setDragAmount_dragAmountLessThanNotifDelayDistance_setsNotificationsScrimProgressToZero() { - val distance = 100 - val delay = 50 - context.orCreateTestableResources.addOverride( - R.dimen.lockscreen_shade_notifications_scrim_transition_distance, - distance - ) - context.orCreateTestableResources.addOverride( - R.dimen.lockscreen_shade_notifications_scrim_transition_delay, - delay - ) - configurationController.notifyConfigurationChanged() + fun setDragAmount_dragAmountLessThanNotifDelayDistance_setsNotificationsScrimProgressToZero() = + testScope.runTest { + val distance = 100 + val delay = 50 + context.orCreateTestableResources.addOverride( + R.dimen.lockscreen_shade_notifications_scrim_transition_distance, + distance + ) + context.orCreateTestableResources.addOverride( + R.dimen.lockscreen_shade_notifications_scrim_transition_delay, + delay + ) + configurationController.notifyConfigurationChanged() - transitionController.dragDownAmount = 20f + transitionController.dragDownAmount = 20f - verify(scrimController) - .transitionToFullShadeProgress( - progress = anyFloat(), - lockScreenNotificationsProgress = eq(0f) - ) - } + verify(scrimController) + .transitionToFullShadeProgress( + progress = anyFloat(), + lockScreenNotificationsProgress = eq(0f) + ) + } @Test - fun setDragAmount_dragAmountMoreThanTotalDistance_setsNotificationsScrimProgressToOne() { - val distance = 100 - val delay = 50 - context.orCreateTestableResources.addOverride( - R.dimen.lockscreen_shade_notifications_scrim_transition_distance, - distance - ) - context.orCreateTestableResources.addOverride( - R.dimen.lockscreen_shade_notifications_scrim_transition_delay, - delay - ) - configurationController.notifyConfigurationChanged() + fun setDragAmount_dragAmountMoreThanTotalDistance_setsNotificationsScrimProgressToOne() = + testScope.runTest { + val distance = 100 + val delay = 50 + context.orCreateTestableResources.addOverride( + R.dimen.lockscreen_shade_notifications_scrim_transition_distance, + distance + ) + context.orCreateTestableResources.addOverride( + R.dimen.lockscreen_shade_notifications_scrim_transition_delay, + delay + ) + configurationController.notifyConfigurationChanged() - transitionController.dragDownAmount = 999999f + transitionController.dragDownAmount = 999999f - verify(scrimController) - .transitionToFullShadeProgress( - progress = anyFloat(), - lockScreenNotificationsProgress = eq(1f) - ) - } + verify(scrimController) + .transitionToFullShadeProgress( + progress = anyFloat(), + lockScreenNotificationsProgress = eq(1f) + ) + } @Test - fun setDragDownAmount_inSplitShade_setsValueOnMediaHierarchyManager() { - enableSplitShade() + fun setDragDownAmount_inSplitShade_setsValueOnMediaHierarchyManager() = + testScope.runTest { + enableSplitShade() - transitionController.dragDownAmount = 10f + transitionController.dragDownAmount = 10f - verify(mediaHierarchyManager).setTransitionToFullShadeAmount(10f) - } + verify(mediaHierarchyManager).setTransitionToFullShadeAmount(10f) + } @Test - fun setDragAmount_notInSplitShade_forwardsToSingleShadeOverScroller() { - disableSplitShade() + fun setDragAmount_notInSplitShade_forwardsToSingleShadeOverScroller() = + testScope.runTest { + disableSplitShade() - transitionController.dragDownAmount = 10f + transitionController.dragDownAmount = 10f - verify(singleShadeOverScroller).expansionDragDownAmount = 10f - verifyZeroInteractions(splitShadeOverScroller) - } + verify(singleShadeOverScroller).expansionDragDownAmount = 10f + verifyZeroInteractions(splitShadeOverScroller) + } @Test - fun setDragAmount_inSplitShade_forwardsToSplitShadeOverScroller() { - enableSplitShade() + fun setDragAmount_inSplitShade_forwardsToSplitShadeOverScroller() = + testScope.runTest { + enableSplitShade() - transitionController.dragDownAmount = 10f + transitionController.dragDownAmount = 10f - verify(splitShadeOverScroller).expansionDragDownAmount = 10f - verifyZeroInteractions(singleShadeOverScroller) - } + verify(splitShadeOverScroller).expansionDragDownAmount = 10f + verifyZeroInteractions(singleShadeOverScroller) + } @Test - fun setDragDownAmount_inSplitShade_setsKeyguardStatusBarAlphaBasedOnDistance() { - val alphaDistance = - context.resources.getDimensionPixelSize( - R.dimen.lockscreen_shade_npvc_keyguard_content_alpha_transition_distance - ) - val dragDownAmount = 10f - enableSplitShade() + fun setDragDownAmount_inSplitShade_setsKeyguardStatusBarAlphaBasedOnDistance() = + testScope.runTest { + val alphaDistance = + context.resources.getDimensionPixelSize( + R.dimen.lockscreen_shade_npvc_keyguard_content_alpha_transition_distance + ) + val dragDownAmount = 10f + enableSplitShade() - transitionController.dragDownAmount = dragDownAmount + transitionController.dragDownAmount = dragDownAmount - val expectedAlpha = 1 - dragDownAmount / alphaDistance - verify(shadeLockscreenInteractor).setKeyguardStatusBarAlpha(expectedAlpha) - } + val expectedAlpha = 1 - dragDownAmount / alphaDistance + verify(shadeLockscreenInteractor).setKeyguardStatusBarAlpha(expectedAlpha) + } @Test - fun setDragDownAmount_notInSplitShade_setsKeyguardStatusBarAlphaToMinusOne() { - disableSplitShade() + fun setDragDownAmount_notInSplitShade_setsKeyguardStatusBarAlphaToMinusOne() = + testScope.runTest { + disableSplitShade() - transitionController.dragDownAmount = 10f + transitionController.dragDownAmount = 10f - verify(shadeLockscreenInteractor).setKeyguardStatusBarAlpha(-1f) - } + verify(shadeLockscreenInteractor).setKeyguardStatusBarAlpha(-1f) + } @Test - fun nullQs_canDragDownFromAdapter() { - transitionController.qS = null + fun nullQs_canDragDownFromAdapter() = + testScope.runTest { + transitionController.qS = null - qsSceneAdapter.isQsFullyCollapsed = true - assertTrue("Can't drag down on keyguard", transitionController.canDragDown()) - qsSceneAdapter.isQsFullyCollapsed = false - assertFalse("Can drag down when QS is expanded", transitionController.canDragDown()) - } + qsSceneAdapter.isQsFullyCollapsed = true + assertTrue("Can't drag down on keyguard", transitionController.canDragDown()) + qsSceneAdapter.isQsFullyCollapsed = false + assertFalse("Can drag down when QS is expanded", transitionController.canDragDown()) + } private fun enableSplitShade() { setSplitShadeEnabled(true) @@ -619,32 +631,4 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { ) { setTransitionToFullShadeProgress(progress, lockScreenNotificationsProgress) } - - @SysUISingleton - @Component( - modules = - [ - SysUITestModule::class, - UserDomainLayerModule::class, - BiometricsDomainLayerModule::class, - ] - ) - interface TestComponent { - - val configurationController: FakeConfigurationController - val disableFlagsRepository: FakeDisableFlagsRepository - val powerInteractor: PowerInteractor - val shadeInteractor: ShadeInteractor - val shadeRepository: FakeShadeRepository - val testScope: TestScope - - @Component.Factory - interface Factory { - fun create( - @BindsInstance test: SysuiTestCase, - featureFlags: FakeFeatureFlagsClassicModule, - mocks: TestMocksModule, - ): TestComponent - } - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt index 8a6a50dd13fc..ecb1a6d44b22 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt @@ -70,7 +70,7 @@ class MediaRouterChipInteractorTest : SysuiTestCase() { } @Test - fun mediaRouterCastingState_connectingDevice_casting() = + fun mediaRouterCastingState_connectingDevice_casting_withName() = testScope.runTest { val latest by collectLastValue(underTest.mediaRouterCastingState) @@ -79,17 +79,18 @@ class MediaRouterChipInteractorTest : SysuiTestCase() { CastDevice( state = CastDevice.CastState.Connecting, id = "id", - name = "name", + name = "My Favorite Device", description = "desc", origin = CastDevice.CastOrigin.MediaRouter, ) ) - assertThat(latest).isEqualTo(MediaRouterCastModel.Casting) + assertThat(latest) + .isEqualTo(MediaRouterCastModel.Casting(deviceName = "My Favorite Device")) } @Test - fun mediaRouterCastingState_connectedDevice_casting() = + fun mediaRouterCastingState_connectedDevice_casting_withName() = testScope.runTest { val latest by collectLastValue(underTest.mediaRouterCastingState) @@ -98,13 +99,14 @@ class MediaRouterChipInteractorTest : SysuiTestCase() { CastDevice( state = CastDevice.CastState.Connected, id = "id", - name = "name", + name = "My Second Favorite Device", description = "desc", origin = CastDevice.CastOrigin.MediaRouter, ) ) - assertThat(latest).isEqualTo(MediaRouterCastModel.Casting) + assertThat(latest) + .isEqualTo(MediaRouterCastModel.Casting(deviceName = "My Second Favorite Device")) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt index e9d6f0e5537a..c8397bdf47c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt @@ -19,8 +19,10 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.view import android.content.ComponentName import android.content.DialogInterface import android.content.Intent +import android.content.applicationContext import android.content.packageManager import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.Kosmos @@ -68,21 +70,87 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) - verify(sysuiDialog).setTitle(R.string.cast_screen_to_other_device_stop_dialog_title) + verify(sysuiDialog).setTitle(R.string.cast_to_other_device_stop_dialog_title) } @Test - fun message_entireScreen() { - createAndSetDelegate(ENTIRE_SCREEN) + fun message_entireScreen_unknownDevice() { + createAndSetDelegate(ENTIRE_SCREEN, deviceName = null) + + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) + + verify(sysuiDialog) + .setMessage( + context.getString(R.string.cast_to_other_device_stop_dialog_message_entire_screen) + ) + } + + @Test + fun message_entireScreen_hasDevice() { + createAndSetDelegate(ENTIRE_SCREEN, deviceName = "My Favorite Device") underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) verify(sysuiDialog) - .setMessage(context.getString(R.string.cast_screen_to_other_device_stop_dialog_message)) + .setMessage( + context.getString( + R.string.cast_to_other_device_stop_dialog_message_entire_screen_with_device, + "My Favorite Device", + ) + ) } @Test - fun message_singleTask() { + fun message_singleTask_unknownAppName_unknownDevice() { + val baseIntent = + Intent().apply { this.component = ComponentName("fake.task.package", "cls") } + whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>())) + .thenThrow(PackageManager.NameNotFoundException()) + + createAndSetDelegate( + MediaProjectionState.Projecting.SingleTask( + HOST_PACKAGE, + createTask(taskId = 1, baseIntent = baseIntent) + ), + deviceName = null, + ) + + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) + + verify(sysuiDialog) + .setMessage( + context.getString(R.string.cast_to_other_device_stop_dialog_message_generic) + ) + } + + @Test + fun message_singleTask_unknownAppName_hasDevice() { + val baseIntent = + Intent().apply { this.component = ComponentName("fake.task.package", "cls") } + whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>())) + .thenThrow(PackageManager.NameNotFoundException()) + + createAndSetDelegate( + MediaProjectionState.Projecting.SingleTask( + HOST_PACKAGE, + createTask(taskId = 1, baseIntent = baseIntent) + ), + deviceName = "My Favorite Device", + ) + + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) + + verify(sysuiDialog) + .setMessage( + context.getString( + R.string.cast_to_other_device_stop_dialog_message_generic_with_device, + "My Favorite Device", + ) + ) + } + + @Test + fun message_singleTask_hasAppName_unknownDevice() { val baseIntent = Intent().apply { this.component = ComponentName("fake.task.package", "cls") } val appInfo = mock<ApplicationInfo>() @@ -94,16 +162,48 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { MediaProjectionState.Projecting.SingleTask( HOST_PACKAGE, createTask(taskId = 1, baseIntent = baseIntent) + ), + deviceName = null, + ) + + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) + + verify(sysuiDialog) + .setMessage( + context.getString( + R.string.cast_to_other_device_stop_dialog_message_specific_app, + "Fake Package", + ) ) + } + + @Test + fun message_singleTask_hasAppName_hasDevice() { + val baseIntent = + Intent().apply { this.component = ComponentName("fake.task.package", "cls") } + val appInfo = mock<ApplicationInfo>() + whenever(appInfo.loadLabel(kosmos.packageManager)).thenReturn("Fake Package") + whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>())) + .thenReturn(appInfo) + + createAndSetDelegate( + MediaProjectionState.Projecting.SingleTask( + HOST_PACKAGE, + createTask(taskId = 1, baseIntent = baseIntent) + ), + deviceName = "My Favorite Device", ) underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) - // It'd be nice to use R.string.cast_screen_to_other_device_stop_dialog_message_specific_app - // directly, but it includes the <b> tags which aren't in the returned string. - val result = argumentCaptor<CharSequence>() - verify(sysuiDialog).setMessage(result.capture()) - assertThat(result.firstValue.toString()).isEqualTo("You will stop casting Fake Package") + verify(sysuiDialog) + .setMessage( + context.getString( + R.string.cast_to_other_device_stop_dialog_message_specific_app_with_device, + "Fake Package", + "My Favorite Device", + ) + ) } @Test @@ -140,14 +240,19 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue() } - private fun createAndSetDelegate(state: MediaProjectionState.Projecting) { + private fun createAndSetDelegate( + state: MediaProjectionState.Projecting, + deviceName: String? = null, + ) { underTest = EndCastScreenToOtherDeviceDialogDelegate( kosmos.endMediaProjectionDialogHelper, + kosmos.applicationContext, stopAction = kosmos.mediaProjectionChipInteractor::stopProjecting, ProjectionChipModel.Projecting( ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE, state, + deviceName, ), ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt index 0af423db7db4..e6101f500ad1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.view import android.content.DialogInterface +import android.content.applicationContext import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue @@ -65,12 +66,30 @@ class EndGenericCastToOtherDeviceDialogDelegateTest : SysuiTestCase() { } @Test - fun message() { - createAndSetDelegate() + fun message_unknownDevice() { + createAndSetDelegate(deviceName = null) + + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) + + verify(sysuiDialog) + .setMessage( + context.getString(R.string.cast_to_other_device_stop_dialog_message_generic) + ) + } + + @Test + fun message_hasDevice() { + createAndSetDelegate(deviceName = "My Favorite Device") underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) - verify(sysuiDialog).setMessage(R.string.cast_to_other_device_stop_dialog_message) + verify(sysuiDialog) + .setMessage( + context.getString( + R.string.cast_to_other_device_stop_dialog_message_generic_with_device, + "My Favorite Device", + ) + ) } @Test @@ -122,10 +141,12 @@ class EndGenericCastToOtherDeviceDialogDelegateTest : SysuiTestCase() { assertThat(kosmos.fakeMediaRouterRepository.lastStoppedDevice).isEqualTo(device) } - private fun createAndSetDelegate() { + private fun createAndSetDelegate(deviceName: String? = null) { underTest = EndGenericCastToOtherDeviceDialogDelegate( kosmos.endMediaProjectionDialogHelper, + kosmos.applicationContext, + deviceName, stopAction = kosmos.mediaRouterChipInteractor::stopCasting, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt index f9ad5ac46b6c..ab935fe9b631 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt @@ -27,7 +27,6 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testCase import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask -import com.android.systemui.res.R import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.google.common.truth.Truth.assertThat @@ -56,19 +55,15 @@ class EndMediaProjectionDialogHelperTest : SysuiTestCase() { } @Test - fun getDialogMessage_entireScreen_isGenericMessage() { + fun getAppName_stateVersion_entireScreen_returnsNull() { val result = - underTest.getDialogMessage( - MediaProjectionState.Projecting.EntireScreen("host.package"), - R.string.accessibility_home, - R.string.cast_screen_to_other_device_stop_dialog_message_specific_app, - ) + underTest.getAppName(MediaProjectionState.Projecting.EntireScreen("host.package")) - assertThat(result).isEqualTo(context.getString(R.string.accessibility_home)) + assertThat(result).isNull() } @Test - fun getDialogMessage_singleTask_cannotFindPackage_isGenericMessage() { + fun getAppName_stateVersion_singleTask_cannotFindPackage_returnsNull() { val baseIntent = Intent().apply { this.component = ComponentName("fake.task.package", "cls") } whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>())) @@ -77,21 +72,16 @@ class EndMediaProjectionDialogHelperTest : SysuiTestCase() { val projectionState = MediaProjectionState.Projecting.SingleTask( "host.package", - createTask(taskId = 1, baseIntent = baseIntent) + createTask(taskId = 1, baseIntent = baseIntent), ) - val result = - underTest.getDialogMessage( - projectionState, - R.string.accessibility_home, - R.string.cast_screen_to_other_device_stop_dialog_message_specific_app, - ) + val result = underTest.getAppName(projectionState) - assertThat(result).isEqualTo(context.getString(R.string.accessibility_home)) + assertThat(result).isNull() } @Test - fun getDialogMessage_singleTask_findsPackage_isSpecificMessageWithAppLabel() { + fun getAppName_stateVersion_singleTask_findsPackage_returnsName() { val baseIntent = Intent().apply { this.component = ComponentName("fake.task.package", "cls") } val appInfo = mock<ApplicationInfo>() @@ -102,93 +92,66 @@ class EndMediaProjectionDialogHelperTest : SysuiTestCase() { val projectionState = MediaProjectionState.Projecting.SingleTask( "host.package", - createTask(taskId = 1, baseIntent = baseIntent) + createTask(taskId = 1, baseIntent = baseIntent), ) - val result = - underTest.getDialogMessage( - projectionState, - R.string.accessibility_home, - R.string.cast_screen_to_other_device_stop_dialog_message_specific_app, - ) + val result = underTest.getAppName(projectionState) - // It'd be nice to use the R.string resources directly, but they include the <b> tags which - // aren't in the returned string. - assertThat(result.toString()).isEqualTo("You will stop casting Fake Package") + assertThat(result).isEqualTo("Fake Package") } @Test - fun getDialogMessage_nullTask_isGenericMessage() { - val result = - underTest.getDialogMessage( - specificTaskInfo = null, - R.string.accessibility_home, - R.string.cast_screen_to_other_device_stop_dialog_message_specific_app, - ) + fun getAppName_taskInfoVersion_null_returnsNull() { + val result = underTest.getAppName(specificTaskInfo = null) - assertThat(result).isEqualTo(context.getString(R.string.accessibility_home)) + assertThat(result).isNull() } @Test - fun getDialogMessage_withTask_cannotFindPackage_isGenericMessage() { + fun getAppName_taskInfoVersion_cannotFindPackage_returnsNull() { val baseIntent = Intent().apply { this.component = ComponentName("fake.task.package", "cls") } whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>())) .thenThrow(PackageManager.NameNotFoundException()) - val task = createTask(taskId = 1, baseIntent = baseIntent) - val result = - underTest.getDialogMessage( - task, - R.string.accessibility_home, - R.string.cast_screen_to_other_device_stop_dialog_message_specific_app, - ) + val result = underTest.getAppName(createTask(taskId = 1, baseIntent = baseIntent)) - assertThat(result).isEqualTo(context.getString(R.string.accessibility_home)) + assertThat(result).isNull() } @Test - fun getDialogMessage_withTask_findsPackage_isSpecificMessageWithAppLabel() { + fun getAppName_taskInfoVersion_findsPackage_returnsName() { val baseIntent = Intent().apply { this.component = ComponentName("fake.task.package", "cls") } val appInfo = mock<ApplicationInfo>() whenever(appInfo.loadLabel(kosmos.packageManager)).thenReturn("Fake Package") whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>())) .thenReturn(appInfo) - val task = createTask(taskId = 1, baseIntent = baseIntent) - val result = - underTest.getDialogMessage( - task, - R.string.accessibility_home, - R.string.cast_screen_to_other_device_stop_dialog_message_specific_app, - ) + val result = underTest.getAppName(createTask(taskId = 1, baseIntent = baseIntent)) - assertThat(result.toString()).isEqualTo("You will stop casting Fake Package") + assertThat(result).isEqualTo("Fake Package") } @Test - fun getDialogMessage_appLabelHasSpecialCharacters_isEscaped() { - val baseIntent = - Intent().apply { this.component = ComponentName("fake.task.package", "cls") } + fun getAppName_packageNameVersion_cannotFindPackage_returnsNull() { + whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>())) + .thenThrow(PackageManager.NameNotFoundException()) + + val result = underTest.getAppName("fake.task.package") + + assertThat(result).isNull() + } + + @Test + fun getAppName_packageNameVersion_findsPackage_returnsName() { val appInfo = mock<ApplicationInfo>() - whenever(appInfo.loadLabel(kosmos.packageManager)).thenReturn("Fake & Package <Here>") + whenever(appInfo.loadLabel(kosmos.packageManager)).thenReturn("Fake Package") whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>())) .thenReturn(appInfo) - val projectionState = - MediaProjectionState.Projecting.SingleTask( - "host.package", - createTask(taskId = 1, baseIntent = baseIntent) - ) - - val result = - underTest.getDialogMessage( - projectionState, - R.string.accessibility_home, - R.string.cast_screen_to_other_device_stop_dialog_message_specific_app, - ) + val result = underTest.getAppName("fake.task.package") - assertThat(result.toString()).isEqualTo("You will stop casting Fake & Package <Here>") + assertThat(result).isEqualTo("Fake Package") } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt index 7e667de3619c..bfb63ac66d3d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt @@ -20,6 +20,7 @@ import android.app.ActivityManager import android.content.ComponentName import android.content.DialogInterface import android.content.Intent +import android.content.applicationContext import android.content.packageManager import android.content.pm.ApplicationInfo import androidx.test.filters.SmallTest @@ -95,11 +96,13 @@ class EndScreenRecordingDialogDelegateTest : SysuiTestCase() { underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) - // It'd be nice to use R.string.screenrecord_stop_dialog_message_specific_app directly, but - // it includes the <b> tags which aren't in the returned string. - val result = argumentCaptor<CharSequence>() - verify(sysuiDialog).setMessage(result.capture()) - assertThat(result.firstValue.toString()).isEqualTo("You will stop recording Fake Package") + verify(sysuiDialog) + .setMessage( + context.getString( + R.string.screenrecord_stop_dialog_message_specific_app, + "Fake Package", + ) + ) } @Test @@ -140,6 +143,7 @@ class EndScreenRecordingDialogDelegateTest : SysuiTestCase() { underTest = EndScreenRecordingDialogDelegate( kosmos.endMediaProjectionDialogHelper, + kosmos.applicationContext, stopAction = kosmos.screenRecordChipInteractor::stopRecording, recordedTask, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt index 63c29ac81b34..bfb57c51206a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt @@ -19,8 +19,10 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.view import android.content.ComponentName import android.content.DialogInterface import android.content.Intent +import android.content.applicationContext import android.content.packageManager import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.Kosmos @@ -65,6 +67,8 @@ class EndShareToAppDialogDelegateTest : SysuiTestCase() { @Test fun title() { createAndSetDelegate(ENTIRE_SCREEN) + whenever(kosmos.packageManager.getApplicationInfo(eq(HOST_PACKAGE), any<Int>())) + .thenThrow(PackageManager.NameNotFoundException()) underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) @@ -72,16 +76,60 @@ class EndShareToAppDialogDelegateTest : SysuiTestCase() { } @Test - fun message_entireScreen() { + fun message_entireScreen_unknownHostPackage() { createAndSetDelegate(ENTIRE_SCREEN) + whenever(kosmos.packageManager.getApplicationInfo(eq(HOST_PACKAGE), any<Int>())) + .thenThrow(PackageManager.NameNotFoundException()) underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) - verify(sysuiDialog).setMessage(context.getString(R.string.share_to_app_stop_dialog_message)) + verify(sysuiDialog) + .setMessage(context.getString(R.string.share_to_app_stop_dialog_message_entire_screen)) } @Test - fun message_singleTask() { + fun message_entireScreen_hasHostPackage() { + createAndSetDelegate(ENTIRE_SCREEN) + val hostAppInfo = mock<ApplicationInfo>() + whenever(hostAppInfo.loadLabel(kosmos.packageManager)).thenReturn("Host Package") + whenever(kosmos.packageManager.getApplicationInfo(eq(HOST_PACKAGE), any<Int>())) + .thenReturn(hostAppInfo) + + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) + + verify(sysuiDialog) + .setMessage( + context.getString( + R.string.share_to_app_stop_dialog_message_entire_screen_with_host_app, + "Host Package", + ) + ) + } + + @Test + fun message_singleTask_unknownAppName() { + val baseIntent = + Intent().apply { this.component = ComponentName("fake.task.package", "cls") } + whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>())) + .thenThrow(PackageManager.NameNotFoundException()) + + createAndSetDelegate( + MediaProjectionState.Projecting.SingleTask( + HOST_PACKAGE, + createTask(taskId = 1, baseIntent = baseIntent) + ) + ) + + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) + + verify(sysuiDialog) + .setMessage( + context.getString(R.string.share_to_app_stop_dialog_message_single_app_generic) + ) + } + + @Test + fun message_singleTask_hasAppName() { val baseIntent = Intent().apply { this.component = ComponentName("fake.task.package", "cls") } val appInfo = mock<ApplicationInfo>() @@ -98,11 +146,13 @@ class EndShareToAppDialogDelegateTest : SysuiTestCase() { underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) - // It'd be nice to use R.string.share_to_app_stop_dialog_message_specific_app directly, but - // it includes the <b> tags which aren't in the returned string. - val result = argumentCaptor<CharSequence>() - verify(sysuiDialog).setMessage(result.capture()) - assertThat(result.firstValue.toString()).isEqualTo("You will stop sharing Fake Package") + verify(sysuiDialog) + .setMessage( + context.getString( + R.string.share_to_app_stop_dialog_message_single_app_specific, + "Fake Package", + ) + ) } @Test @@ -118,6 +168,8 @@ class EndShareToAppDialogDelegateTest : SysuiTestCase() { fun positiveButton() = kosmos.testScope.runTest { createAndSetDelegate(ENTIRE_SCREEN) + whenever(kosmos.packageManager.getApplicationInfo(eq(HOST_PACKAGE), any<Int>())) + .thenThrow(PackageManager.NameNotFoundException()) underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) @@ -143,8 +195,13 @@ class EndShareToAppDialogDelegateTest : SysuiTestCase() { underTest = EndShareToAppDialogDelegate( kosmos.endMediaProjectionDialogHelper, + kosmos.applicationContext, stopAction = kosmos.mediaProjectionChipInteractor::stopProjecting, - ProjectionChipModel.Projecting(ProjectionChipModel.Type.SHARE_TO_APP, state), + ProjectionChipModel.Projecting( + ProjectionChipModel.Type.SHARE_TO_APP, + state, + deviceName = null, + ), ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt index 26f53707aba1..f07303ed32e3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt @@ -17,21 +17,19 @@ package com.android.systemui.statusbar.notification.icon.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.SysUITestComponent -import com.android.systemui.SysUITestModule import com.android.systemui.SysuiTestCase -import com.android.systemui.TestMocksModule -import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule -import com.android.systemui.collectLastValue -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository -import com.android.systemui.runTest -import com.android.systemui.statusbar.data.repository.NotificationListenerSettingsRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.data.repository.notificationListenerSettingsRepository import com.android.systemui.statusbar.notification.data.model.activeNotificationModel -import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore -import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationIconInteractor -import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor +import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository +import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository +import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor +import com.android.systemui.statusbar.notification.domain.interactor.headsUpNotificationIconInteractor import com.android.systemui.statusbar.notification.shared.byIsAmbient import com.android.systemui.statusbar.notification.shared.byIsLastMessageFromReply import com.android.systemui.statusbar.notification.shared.byIsPulsing @@ -39,15 +37,15 @@ import com.android.systemui.statusbar.notification.shared.byIsRowDismissed import com.android.systemui.statusbar.notification.shared.byIsSilent import com.android.systemui.statusbar.notification.shared.byIsSuppressedFromStatusBar import com.android.systemui.statusbar.notification.shared.byKey -import com.android.systemui.user.domain.UserDomainLayerModule +import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever -import com.android.wm.shell.bubbles.Bubbles +import com.android.wm.shell.bubbles.bubbles +import com.android.wm.shell.bubbles.bubblesOptional import com.google.common.truth.Truth.assertThat -import dagger.BindsInstance -import dagger.Component -import java.util.Optional +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -55,29 +53,22 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class NotificationIconsInteractorTest : SysuiTestCase() { - - private val bubbles: Bubbles = mock() - - @Component(modules = [SysUITestModule::class]) - @SysUISingleton - interface TestComponent : SysUITestComponent<NotificationIconsInteractor> { - - val activeNotificationListRepository: ActiveNotificationListRepository - val notificationsKeyguardInteractor: NotificationsKeyguardInteractor - - @Component.Factory - interface Factory { - fun create(@BindsInstance test: SysuiTestCase, mocks: TestMocksModule): TestComponent - } - } - - val testComponent: TestComponent = - DaggerNotificationIconsInteractorTest_TestComponent.factory() - .create(test = this, mocks = TestMocksModule(bubbles = Optional.of(bubbles))) + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val activeNotificationListRepository = kosmos.activeNotificationListRepository + private val notificationsKeyguardInteractor = kosmos.notificationsKeyguardInteractor + + private val underTest = + NotificationIconsInteractor( + kosmos.activeNotificationsInteractor, + kosmos.bubblesOptional, + kosmos.headsUpNotificationIconInteractor, + kosmos.notificationsKeyguardViewStateRepository + ) @Before fun setup() { - testComponent.apply { + testScope.apply { activeNotificationListRepository.activeNotifications.value = ActiveNotificationsStore.Builder() .apply { testIcons.forEach(::addIndividualNotif) } @@ -87,22 +78,22 @@ class NotificationIconsInteractorTest : SysuiTestCase() { @Test fun filteredEntrySet() = - testComponent.runTest { + testScope.runTest { val filteredSet by collectLastValue(underTest.filteredNotifSet()) assertThat(filteredSet).containsExactlyElementsIn(testIcons) } @Test fun filteredEntrySet_noExpandedBubbles() = - testComponent.runTest { - whenever(bubbles.isBubbleExpanded(eq("notif1"))).thenReturn(true) + testScope.runTest { + whenever(kosmos.bubbles.isBubbleExpanded(eq("notif1"))).thenReturn(true) val filteredSet by collectLastValue(underTest.filteredNotifSet()) assertThat(filteredSet).comparingElementsUsing(byKey).doesNotContain("notif1") } @Test fun filteredEntrySet_noAmbient() = - testComponent.runTest { + testScope.runTest { val filteredSet by collectLastValue(underTest.filteredNotifSet(showAmbient = false)) assertThat(filteredSet).comparingElementsUsing(byIsAmbient).doesNotContain(true) assertThat(filteredSet) @@ -112,21 +103,21 @@ class NotificationIconsInteractorTest : SysuiTestCase() { @Test fun filteredEntrySet_noLowPriority() = - testComponent.runTest { + testScope.runTest { val filteredSet by collectLastValue(underTest.filteredNotifSet(showLowPriority = false)) assertThat(filteredSet).comparingElementsUsing(byIsSilent).doesNotContain(true) } @Test fun filteredEntrySet_noDismissed() = - testComponent.runTest { + testScope.runTest { val filteredSet by collectLastValue(underTest.filteredNotifSet(showDismissed = false)) assertThat(filteredSet).comparingElementsUsing(byIsRowDismissed).doesNotContain(true) } @Test fun filteredEntrySet_noRepliedMessages() = - testComponent.runTest { + testScope.runTest { val filteredSet by collectLastValue(underTest.filteredNotifSet(showRepliedMessages = false)) assertThat(filteredSet) @@ -136,7 +127,7 @@ class NotificationIconsInteractorTest : SysuiTestCase() { @Test fun filteredEntrySet_noPulsing_notifsNotFullyHidden() = - testComponent.runTest { + testScope.runTest { val filteredSet by collectLastValue(underTest.filteredNotifSet(showPulsing = false)) notificationsKeyguardInteractor.setNotificationsFullyHidden(false) assertThat(filteredSet).comparingElementsUsing(byIsPulsing).doesNotContain(true) @@ -144,65 +135,46 @@ class NotificationIconsInteractorTest : SysuiTestCase() { @Test fun filteredEntrySet_noPulsing_notifsFullyHidden() = - testComponent.runTest { + testScope.runTest { val filteredSet by collectLastValue(underTest.filteredNotifSet(showPulsing = false)) notificationsKeyguardInteractor.setNotificationsFullyHidden(true) assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true) } } +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class AlwaysOnDisplayNotificationIconsInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope - private val bubbles: Bubbles = mock() - - @Component( - modules = - [ - SysUITestModule::class, - BiometricsDomainLayerModule::class, - UserDomainLayerModule::class, - ] - ) - @SysUISingleton - interface TestComponent : SysUITestComponent<AlwaysOnDisplayNotificationIconsInteractor> { - - val activeNotificationListRepository: ActiveNotificationListRepository - val deviceEntryRepository: FakeDeviceEntryRepository - val notificationsKeyguardInteractor: NotificationsKeyguardInteractor - - @Component.Factory - interface Factory { - fun create(@BindsInstance test: SysuiTestCase, mocks: TestMocksModule): TestComponent - } - } - - private val testComponent: TestComponent = - DaggerAlwaysOnDisplayNotificationIconsInteractorTest_TestComponent.factory() - .create(test = this, mocks = TestMocksModule(bubbles = Optional.of(bubbles))) + private val underTest = + AlwaysOnDisplayNotificationIconsInteractor( + kosmos.testDispatcher, + kosmos.deviceEntryInteractor, + kosmos.notificationIconsInteractor, + ) @Before fun setup() { - testComponent.apply { - activeNotificationListRepository.activeNotifications.value = - ActiveNotificationsStore.Builder() - .apply { testIcons.forEach(::addIndividualNotif) } - .build() - } + kosmos.activeNotificationListRepository.activeNotifications.value = + ActiveNotificationsStore.Builder() + .apply { testIcons.forEach(::addIndividualNotif) } + .build() } @Test fun filteredEntrySet_noExpandedBubbles() = - testComponent.runTest { - whenever(bubbles.isBubbleExpanded(eq("notif1"))).thenReturn(true) + testScope.runTest { + whenever(kosmos.bubbles.isBubbleExpanded(eq("notif1"))).thenReturn(true) val filteredSet by collectLastValue(underTest.aodNotifs) assertThat(filteredSet).comparingElementsUsing(byKey).doesNotContain("notif1") } @Test fun filteredEntrySet_noAmbient() = - testComponent.runTest { + testScope.runTest { val filteredSet by collectLastValue(underTest.aodNotifs) assertThat(filteredSet).comparingElementsUsing(byIsAmbient).doesNotContain(true) assertThat(filteredSet) @@ -212,14 +184,14 @@ class AlwaysOnDisplayNotificationIconsInteractorTest : SysuiTestCase() { @Test fun filteredEntrySet_noDismissed() = - testComponent.runTest { + testScope.runTest { val filteredSet by collectLastValue(underTest.aodNotifs) assertThat(filteredSet).comparingElementsUsing(byIsRowDismissed).doesNotContain(true) } @Test fun filteredEntrySet_noRepliedMessages() = - testComponent.runTest { + testScope.runTest { val filteredSet by collectLastValue(underTest.aodNotifs) assertThat(filteredSet) .comparingElementsUsing(byIsLastMessageFromReply) @@ -228,37 +200,37 @@ class AlwaysOnDisplayNotificationIconsInteractorTest : SysuiTestCase() { @Test fun filteredEntrySet_showPulsing_notifsNotFullyHidden_bypassDisabled() = - testComponent.runTest { + testScope.runTest { val filteredSet by collectLastValue(underTest.aodNotifs) - deviceEntryRepository.setBypassEnabled(false) - notificationsKeyguardInteractor.setNotificationsFullyHidden(false) + kosmos.fakeDeviceEntryRepository.setBypassEnabled(false) + kosmos.notificationsKeyguardInteractor.setNotificationsFullyHidden(false) assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true) } @Test fun filteredEntrySet_showPulsing_notifsFullyHidden_bypassDisabled() = - testComponent.runTest { + testScope.runTest { val filteredSet by collectLastValue(underTest.aodNotifs) - deviceEntryRepository.setBypassEnabled(false) - notificationsKeyguardInteractor.setNotificationsFullyHidden(true) + kosmos.fakeDeviceEntryRepository.setBypassEnabled(false) + kosmos.notificationsKeyguardInteractor.setNotificationsFullyHidden(true) assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true) } @Test fun filteredEntrySet_noPulsing_notifsNotFullyHidden_bypassEnabled() = - testComponent.runTest { + testScope.runTest { val filteredSet by collectLastValue(underTest.aodNotifs) - deviceEntryRepository.setBypassEnabled(true) - notificationsKeyguardInteractor.setNotificationsFullyHidden(false) + kosmos.fakeDeviceEntryRepository.setBypassEnabled(true) + kosmos.notificationsKeyguardInteractor.setNotificationsFullyHidden(false) assertThat(filteredSet).comparingElementsUsing(byIsPulsing).doesNotContain(true) } @Test fun filteredEntrySet_showPulsing_notifsFullyHidden_bypassEnabled() = - testComponent.runTest { + testScope.runTest { val filteredSet by collectLastValue(underTest.aodNotifs) - deviceEntryRepository.setBypassEnabled(true) - notificationsKeyguardInteractor.setNotificationsFullyHidden(true) + kosmos.fakeDeviceEntryRepository.setBypassEnabled(true) + kosmos.notificationsKeyguardInteractor.setNotificationsFullyHidden(true) assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true) } } @@ -266,32 +238,19 @@ class AlwaysOnDisplayNotificationIconsInteractorTest : SysuiTestCase() { @SmallTest @RunWith(AndroidJUnit4::class) class StatusBarNotificationIconsInteractorTest : SysuiTestCase() { - - private val bubbles: Bubbles = mock() - - @Component(modules = [SysUITestModule::class]) - @SysUISingleton - interface TestComponent : SysUITestComponent<StatusBarNotificationIconsInteractor> { - - val activeNotificationListRepository: ActiveNotificationListRepository - val headsUpIconsInteractor: HeadsUpNotificationIconInteractor - val notificationsKeyguardInteractor: NotificationsKeyguardInteractor - val notificationListenerSettingsRepository: NotificationListenerSettingsRepository - - @Component.Factory - interface Factory { - fun create(@BindsInstance test: SysuiTestCase, mocks: TestMocksModule): TestComponent - } - } - - val testComponent: TestComponent = - DaggerStatusBarNotificationIconsInteractorTest_TestComponent.factory() - .create(test = this, mocks = TestMocksModule(bubbles = Optional.of(bubbles))) + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val underTest = + StatusBarNotificationIconsInteractor( + kosmos.testDispatcher, + kosmos.notificationIconsInteractor, + kosmos.notificationListenerSettingsRepository, + ) @Before fun setup() { - testComponent.apply { - activeNotificationListRepository.activeNotifications.value = + testScope.apply { + kosmos.activeNotificationListRepository.activeNotifications.value = ActiveNotificationsStore.Builder() .apply { testIcons.forEach(::addIndividualNotif) } .build() @@ -300,15 +259,15 @@ class StatusBarNotificationIconsInteractorTest : SysuiTestCase() { @Test fun filteredEntrySet_noExpandedBubbles() = - testComponent.runTest { - whenever(bubbles.isBubbleExpanded(eq("notif1"))).thenReturn(true) + testScope.runTest { + whenever(kosmos.bubbles.isBubbleExpanded(eq("notif1"))).thenReturn(true) val filteredSet by collectLastValue(underTest.statusBarNotifs) assertThat(filteredSet).comparingElementsUsing(byKey).doesNotContain("notif1") } @Test fun filteredEntrySet_noAmbient() = - testComponent.runTest { + testScope.runTest { val filteredSet by collectLastValue(underTest.statusBarNotifs) assertThat(filteredSet).comparingElementsUsing(byIsAmbient).doesNotContain(true) assertThat(filteredSet) @@ -318,30 +277,30 @@ class StatusBarNotificationIconsInteractorTest : SysuiTestCase() { @Test fun filteredEntrySet_noLowPriority_whenDontShowSilentIcons() = - testComponent.runTest { + testScope.runTest { val filteredSet by collectLastValue(underTest.statusBarNotifs) - notificationListenerSettingsRepository.showSilentStatusIcons.value = false + kosmos.notificationListenerSettingsRepository.showSilentStatusIcons.value = false assertThat(filteredSet).comparingElementsUsing(byIsSilent).doesNotContain(true) } @Test fun filteredEntrySet_showLowPriority_whenShowSilentIcons() = - testComponent.runTest { + testScope.runTest { val filteredSet by collectLastValue(underTest.statusBarNotifs) - notificationListenerSettingsRepository.showSilentStatusIcons.value = true + kosmos.notificationListenerSettingsRepository.showSilentStatusIcons.value = true assertThat(filteredSet).comparingElementsUsing(byIsSilent).contains(true) } @Test fun filteredEntrySet_noDismissed() = - testComponent.runTest { + testScope.runTest { val filteredSet by collectLastValue(underTest.statusBarNotifs) assertThat(filteredSet).comparingElementsUsing(byIsRowDismissed).doesNotContain(true) } @Test fun filteredEntrySet_noRepliedMessages() = - testComponent.runTest { + testScope.runTest { val filteredSet by collectLastValue(underTest.statusBarNotifs) assertThat(filteredSet) .comparingElementsUsing(byIsLastMessageFromReply) @@ -350,9 +309,9 @@ class StatusBarNotificationIconsInteractorTest : SysuiTestCase() { @Test fun filteredEntrySet_includesIsolatedIcon() = - testComponent.runTest { + testScope.runTest { val filteredSet by collectLastValue(underTest.statusBarNotifs) - headsUpIconsInteractor.setIsolatedIconNotificationKey("notif5") + kosmos.headsUpNotificationIconInteractor.setIsolatedIconNotificationKey("notif5") assertThat(filteredSet).comparingElementsUsing(byKey).contains("notif5") } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt index 894e02e80997..1f4e80e48bb7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt @@ -16,111 +16,81 @@ package com.android.systemui.statusbar.notification.icon.ui.viewmodel +import android.content.res.mainResources import android.platform.test.annotations.DisableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR import com.android.systemui.Flags.FLAG_NEW_AOD_TRANSITION -import com.android.systemui.SysUITestComponent -import com.android.systemui.SysUITestModule import com.android.systemui.SysuiTestCase -import com.android.systemui.TestMocksModule -import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule -import com.android.systemui.collectLastValue -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.flags.FakeFeatureFlagsClassicModule +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.DozeTransitionModel import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep -import com.android.systemui.power.data.repository.FakePowerRepository +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.power.shared.model.WakefulnessState -import com.android.systemui.runCurrent -import com.android.systemui.runTest -import com.android.systemui.statusbar.phone.DozeParameters -import com.android.systemui.statusbar.phone.ScreenOffAnimationController -import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository -import com.android.systemui.user.domain.UserDomainLayerModule -import com.android.systemui.util.mockito.mock +import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.notification.icon.domain.interactor.alwaysOnDisplayNotificationIconsInteractor +import com.android.systemui.statusbar.phone.dozeParameters +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import dagger.BindsInstance -import dagger.Component +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { - - @SysUISingleton - @Component( - modules = - [ - SysUITestModule::class, - BiometricsDomainLayerModule::class, - UserDomainLayerModule::class, - ] - ) - interface TestComponent : - SysUITestComponent<NotificationIconContainerAlwaysOnDisplayViewModel> { - - val deviceProvisioningRepository: FakeDeviceProvisioningRepository - val keyguardRepository: FakeKeyguardRepository - val keyguardTransitionRepository: FakeKeyguardTransitionRepository - val powerRepository: FakePowerRepository - - @Component.Factory - interface Factory { - fun create( - @BindsInstance test: SysuiTestCase, - mocks: TestMocksModule, - featureFlags: FakeFeatureFlagsClassicModule, - ): TestComponent + private val kosmos = + testKosmos().apply { + fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, value = false) } } - } - private val dozeParams: DozeParameters = mock() - private val screenOffAnimController: ScreenOffAnimationController = mock() - - private val testComponent: TestComponent = - DaggerNotificationIconContainerAlwaysOnDisplayViewModelTest_TestComponent.factory() - .create( - test = this, - featureFlags = - FakeFeatureFlagsClassicModule { - set(Flags.FULL_SCREEN_USER_SWITCHER, value = false) - }, - mocks = - TestMocksModule( - dozeParameters = dozeParams, - screenOffAnimationController = screenOffAnimController, - ), - ) + val underTest = + NotificationIconContainerAlwaysOnDisplayViewModel( + kosmos.testDispatcher, + kosmos.alwaysOnDisplayNotificationIconsInteractor, + kosmos.keyguardInteractor, + kosmos.keyguardTransitionInteractor, + kosmos.mainResources, + kosmos.shadeInteractor, + ) + val testScope = kosmos.testScope + val keyguardRepository = kosmos.fakeKeyguardRepository + val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + val powerRepository = kosmos.fakePowerRepository @Before fun setup() { - testComponent.apply { - keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setKeyguardOccluded(false) - powerRepository.updateWakefulness( - rawState = WakefulnessState.AWAKE, - lastWakeReason = WakeSleepReason.OTHER, - lastSleepReason = WakeSleepReason.OTHER, - ) - } + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setKeyguardOccluded(false) + kosmos.fakePowerRepository.updateWakefulness( + rawState = WakefulnessState.AWAKE, + lastWakeReason = WakeSleepReason.OTHER, + lastSleepReason = WakeSleepReason.OTHER, + ) mSetFlagsRule.enableFlags(FLAG_NEW_AOD_TRANSITION) } @Test fun animationsEnabled_isFalse_whenDeviceAsleepAndNotPulsing() = - testComponent.runTest { + testScope.runTest { powerRepository.updateWakefulness( rawState = WakefulnessState.ASLEEP, lastWakeReason = WakeSleepReason.POWER_BUTTON, @@ -143,7 +113,7 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { @Test fun animationsEnabled_isTrue_whenDeviceAsleepAndPulsing() = - testComponent.runTest { + testScope.runTest { powerRepository.updateWakefulness( rawState = WakefulnessState.ASLEEP, lastWakeReason = WakeSleepReason.POWER_BUTTON, @@ -166,7 +136,7 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { @Test fun animationsEnabled_isFalse_whenStartingToSleepAndNotControlScreenOff() = - testComponent.runTest { + testScope.runTest { powerRepository.updateWakefulness( rawState = WakefulnessState.STARTING_TO_SLEEP, lastWakeReason = WakeSleepReason.POWER_BUTTON, @@ -179,7 +149,7 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { transitionState = TransitionState.STARTED, ) ) - whenever(dozeParams.shouldControlScreenOff()).thenReturn(false) + whenever(kosmos.dozeParameters.shouldControlScreenOff()).thenReturn(false) val animationsEnabled by collectLastValue(underTest.areContainerChangesAnimated) runCurrent() assertThat(animationsEnabled).isFalse() @@ -187,7 +157,7 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { @Test fun animationsEnabled_isTrue_whenStartingToSleepAndControlScreenOff() = - testComponent.runTest { + testScope.runTest { val animationsEnabled by collectLastValue(underTest.areContainerChangesAnimated) assertThat(animationsEnabled).isTrue() @@ -203,13 +173,13 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { transitionState = TransitionState.STARTED, ) ) - whenever(dozeParams.shouldControlScreenOff()).thenReturn(true) + whenever(kosmos.dozeParameters.shouldControlScreenOff()).thenReturn(true) assertThat(animationsEnabled).isTrue() } @Test fun animationsEnabled_isTrue_whenNotAsleep() = - testComponent.runTest { + testScope.runTest { powerRepository.updateWakefulness( rawState = WakefulnessState.AWAKE, lastWakeReason = WakeSleepReason.POWER_BUTTON, @@ -228,7 +198,7 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { @Test @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) fun animationsEnabled_isTrue_whenKeyguardIsShowing() = - testComponent.runTest { + testScope.runTest { keyguardTransitionRepository.sendTransitionStep( TransitionStep( transitionState = TransitionState.STARTED, @@ -257,7 +227,7 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { @Test fun tintAlpha_isZero_whenNotOnAodOrDozing() = - testComponent.runTest { + testScope.runTest { val tintAlpha by collectLastValue(underTest.tintAlpha) runCurrent() keyguardTransitionRepository.sendTransitionSteps( @@ -271,7 +241,7 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { @Test fun tintAlpha_isOne_whenOnAod() = - testComponent.runTest { + testScope.runTest { val tintAlpha by collectLastValue(underTest.tintAlpha) runCurrent() keyguardTransitionRepository.sendTransitionSteps( @@ -285,7 +255,7 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { @Test fun tintAlpha_isOne_whenDozing() = - testComponent.runTest { + testScope.runTest { val tintAlpha by collectLastValue(underTest.tintAlpha) runCurrent() keyguardTransitionRepository.sendTransitionSteps( @@ -298,7 +268,7 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { @Test fun tintAlpha_isOne_whenTransitionFromAodToDoze() = - testComponent.runTest { + testScope.runTest { keyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.GONE, to = KeyguardState.AOD, @@ -332,7 +302,7 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { @Test fun tintAlpha_isFraction_midTransitionToAod() = - testComponent.runTest { + testScope.runTest { val tintAlpha by collectLastValue(underTest.tintAlpha) runCurrent() @@ -361,7 +331,7 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { @Test fun iconAnimationsEnabled_whenOnLockScreen() = - testComponent.runTest { + testScope.runTest { val iconAnimationsEnabled by collectLastValue(underTest.areIconAnimationsEnabled) runCurrent() @@ -376,7 +346,7 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { @Test fun iconAnimationsDisabled_whenOnAod() = - testComponent.runTest { + testScope.runTest { val iconAnimationsEnabled by collectLastValue(underTest.areIconAnimationsEnabled) runCurrent() @@ -391,7 +361,7 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { @Test fun iconAnimationsDisabled_whenDozing() = - testComponent.runTest { + testScope.runTest { val iconAnimationsEnabled by collectLastValue(underTest.areIconAnimationsEnabled) runCurrent() diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java deleted file mode 100644 index ef10fdf63741..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright (C) 2022 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.util.service; - -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.util.concurrency.FakeExecutor; -import com.android.systemui.util.time.FakeSystemClock; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidJUnit4.class) -public class PersistentConnectionManagerTest extends SysuiTestCase { - private static final int MAX_RETRIES = 5; - private static final int RETRY_DELAY_MS = 1000; - private static final int CONNECTION_MIN_DURATION_MS = 5000; - private static final String DUMPSYS_NAME = "dumpsys_name"; - - private FakeSystemClock mFakeClock = new FakeSystemClock(); - private FakeExecutor mFakeExecutor = new FakeExecutor(mFakeClock); - - @Mock - private ObservableServiceConnection<Proxy> mConnection; - - @Mock - private ObservableServiceConnection.Callback<Proxy> mConnectionCallback; - - @Mock - private Observer mObserver; - - @Mock - private DumpManager mDumpManager; - - private static class Proxy { - } - - private PersistentConnectionManager<Proxy> mConnectionManager; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - - mConnectionManager = new PersistentConnectionManager<>( - mFakeClock, - mFakeExecutor, - mDumpManager, - DUMPSYS_NAME, - mConnection, - MAX_RETRIES, - RETRY_DELAY_MS, - CONNECTION_MIN_DURATION_MS, - mObserver); - } - - private ObservableServiceConnection.Callback<Proxy> captureCallbackAndSend( - ObservableServiceConnection<Proxy> mConnection, Proxy proxy) { - ArgumentCaptor<ObservableServiceConnection.Callback<Proxy>> connectionCallbackCaptor = - ArgumentCaptor.forClass(ObservableServiceConnection.Callback.class); - - verify(mConnection).addCallback(connectionCallbackCaptor.capture()); - verify(mConnection).bind(); - Mockito.clearInvocations(mConnection); - - final ObservableServiceConnection.Callback callback = connectionCallbackCaptor.getValue(); - if (proxy != null) { - callback.onConnected(mConnection, proxy); - } else { - callback.onDisconnected(mConnection, 0); - } - - return callback; - } - - /** - * Validates initial connection. - */ - @Test - public void testConnect() { - mConnectionManager.start(); - captureCallbackAndSend(mConnection, Mockito.mock(Proxy.class)); - } - - /** - * Ensures reconnection on disconnect. - */ - @Test - public void testRetryOnBindFailure() { - mConnectionManager.start(); - ArgumentCaptor<ObservableServiceConnection.Callback<Proxy>> connectionCallbackCaptor = - ArgumentCaptor.forClass(ObservableServiceConnection.Callback.class); - - verify(mConnection).addCallback(connectionCallbackCaptor.capture()); - - // Verify attempts happen. Note that we account for the retries plus initial attempt, which - // is not scheduled. - for (int attemptCount = 0; attemptCount < MAX_RETRIES + 1; attemptCount++) { - verify(mConnection).bind(); - Mockito.clearInvocations(mConnection); - connectionCallbackCaptor.getValue().onDisconnected(mConnection, 0); - mFakeExecutor.advanceClockToNext(); - mFakeExecutor.runAllReady(); - } - } - - /** - * Ensures manual unbind does not reconnect. - */ - @Test - public void testStopDoesNotReconnect() { - mConnectionManager.start(); - ArgumentCaptor<ObservableServiceConnection.Callback<Proxy>> connectionCallbackCaptor = - ArgumentCaptor.forClass(ObservableServiceConnection.Callback.class); - - verify(mConnection).addCallback(connectionCallbackCaptor.capture()); - verify(mConnection).bind(); - Mockito.clearInvocations(mConnection); - mConnectionManager.stop(); - mFakeExecutor.advanceClockToNext(); - mFakeExecutor.runAllReady(); - verify(mConnection, never()).bind(); - } - - /** - * Ensures rebind on package change. - */ - @Test - public void testAttemptOnPackageChange() { - mConnectionManager.start(); - verify(mConnection).bind(); - ArgumentCaptor<Observer.Callback> callbackCaptor = - ArgumentCaptor.forClass(Observer.Callback.class); - captureCallbackAndSend(mConnection, Mockito.mock(Proxy.class)); - - verify(mObserver).addCallback(callbackCaptor.capture()); - - callbackCaptor.getValue().onSourceChanged(); - verify(mConnection).bind(); - } - - @Test - public void testAddConnectionCallback() { - mConnectionManager.addConnectionCallback(mConnectionCallback); - verify(mConnection).addCallback(mConnectionCallback); - } - - @Test - public void testRemoveConnectionCallback() { - mConnectionManager.removeConnectionCallback(mConnectionCallback); - verify(mConnection).removeCallback(mConnectionCallback); - } -} diff --git a/packages/SystemUI/tests/utils/src/android/app/admin/AlarmManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/app/admin/AlarmManagerKosmos.kt new file mode 100644 index 000000000000..a7b5873a4798 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/android/app/admin/AlarmManagerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.admin + +import android.app.AlarmManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.alarmManager by Kosmos.Fixture { mock<AlarmManager>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/app/admin/DevicePolicyManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/app/admin/DevicePolicyManagerKosmos.kt new file mode 100644 index 000000000000..f51e122be214 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/app/admin/DevicePolicyManagerKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.app.admin + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +val Kosmos.devicePolicyManager by Kosmos.Fixture { mock<android.app.admin.DevicePolicyManager>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/internal/widget/LockPatternUtilsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/internal/widget/LockPatternUtilsKosmos.kt index d9ea5e92710c..b511270cdf09 100644 --- a/packages/SystemUI/tests/utils/src/com/android/internal/widget/LockPatternUtilsKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/internal/widget/LockPatternUtilsKosmos.kt @@ -16,7 +16,14 @@ package com.android.internal.widget +import android.app.admin.devicePolicyManager import com.android.systemui.kosmos.Kosmos import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever -var Kosmos.lockPatternUtils by Kosmos.Fixture { mock<LockPatternUtils>() } +var Kosmos.lockPatternUtils by + Kosmos.Fixture { + mock<LockPatternUtils>().apply { + whenever(this.devicePolicyManager).thenReturn(this@Fixture.devicePolicyManager) + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 5bae6ec89b65..87143efb7f3c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -135,6 +135,9 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { private var isShowKeyguardWhenReenabled: Boolean = false + private val _canIgnoreAuthAndReturnToGone = MutableStateFlow(false) + override val canIgnoreAuthAndReturnToGone = _canIgnoreAuthAndReturnToGone.asStateFlow() + override fun setQuickSettingsVisible(isVisible: Boolean) { _isQuickSettingsVisible.value = isVisible } @@ -278,6 +281,10 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { override fun isShowKeyguardWhenReenabled(): Boolean { return isShowKeyguardWhenReenabled } + + override fun setCanIgnoreAuthAndReturnToGone(canWake: Boolean) { + _canIgnoreAuthAndReturnToGone.value = canWake + } } @Module diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt index ae138c8f930b..ef789d1b29ff 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt @@ -37,5 +37,6 @@ val Kosmos.fromAodTransitionInteractor by powerInteractor = powerInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, deviceEntryRepository = deviceEntryRepository, + wakeToGoneInteractor = keyguardWakeDirectlyToGoneInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt index e7e007fd79fa..446652c7c6d8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt @@ -39,5 +39,6 @@ var Kosmos.fromDozingTransitionInteractor by powerInteractor = powerInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, deviceEntryRepository = deviceEntryRepository, + wakeToGoneInteractor = keyguardWakeDirectlyToGoneInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorKosmos.kt index a9be06d6387f..6c3de443be79 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorKosmos.kt @@ -16,13 +16,16 @@ package com.android.systemui.keyguard.domain.interactor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor +import kotlinx.coroutines.ExperimentalCoroutinesApi +@OptIn(ExperimentalCoroutinesApi::class) var Kosmos.fromDreamingTransitionInteractor by Kosmos.Fixture { FromDreamingTransitionInteractor( @@ -36,5 +39,6 @@ var Kosmos.fromDreamingTransitionInteractor by glanceableHubTransitions = glanceableHubTransitions, powerInteractor = powerInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, + deviceEntryInteractor = deviceEntryInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorKosmos.kt new file mode 100644 index 000000000000..63e168d018bf --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorKosmos.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.app.admin.alarmManager +import android.content.mockedContext +import com.android.internal.widget.lockPatternUtils +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.user.domain.interactor.selectedUserInteractor +import com.android.systemui.util.settings.fakeSettings +import com.android.systemui.util.time.systemClock + +val Kosmos.keyguardWakeDirectlyToGoneInteractor by + Kosmos.Fixture { + KeyguardWakeDirectlyToGoneInteractor( + applicationCoroutineScope, + mockedContext, + fakeKeyguardRepository, + systemClock, + alarmManager, + keyguardTransitionInteractor, + powerInteractor, + fakeSettings, + lockPatternUtils, + fakeSettings, + selectedUserInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorKosmos.kt index bd9c0be6b0b7..8bb2fcecca58 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.domain.interactor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.statusbar.notification.domain.interactor.notificationLaunchAnimationInteractor @@ -25,6 +26,7 @@ val Kosmos.windowManagerLockscreenVisibilityInteractor by Kosmos.Fixture { WindowManagerLockscreenVisibilityInteractor( keyguardInteractor = keyguardInteractor, + transitionRepository = keyguardTransitionRepository, transitionInteractor = keyguardTransitionInteractor, surfaceBehindInteractor = keyguardSurfaceBehindInteractor, fromLockscreenInteractor = fromLockscreenTransitionInteractor, @@ -33,5 +35,6 @@ val Kosmos.windowManagerLockscreenVisibilityInteractor by notificationLaunchAnimationInteractor = notificationLaunchAnimationInteractor, sceneInteractor = { sceneInteractor }, deviceEntryInteractor = { deviceEntryInteractor }, + wakeToGoneInteractor = keyguardWakeDirectlyToGoneInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt index a8de4607e4a2..144fe26ea230 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel +import android.content.applicationContext import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope @@ -28,6 +29,7 @@ val Kosmos.castToOtherDeviceChipViewModel: CastToOtherDeviceChipViewModel by Kosmos.Fixture { CastToOtherDeviceChipViewModel( scope = applicationCoroutineScope, + context = applicationContext, mediaProjectionChipInteractor = mediaProjectionChipInteractor, mediaRouterChipInteractor = mediaRouterChipInteractor, systemClock = fakeSystemClock, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt index 4f82662fa673..1ed7a4702e2c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.chips.mediaprojection.ui.view -import android.content.applicationContext import android.content.packageManager import com.android.systemui.kosmos.Kosmos import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory @@ -26,6 +25,5 @@ val Kosmos.endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper by EndMediaProjectionDialogHelper( dialogFactory = mockSystemUIDialogFactory, packageManager = packageManager, - context = applicationContext, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt index 99b7ec9c9f1a..1d06947a40da 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel +import android.content.applicationContext import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope @@ -27,6 +28,7 @@ val Kosmos.screenRecordChipViewModel: ScreenRecordChipViewModel by Kosmos.Fixture { ScreenRecordChipViewModel( scope = applicationCoroutineScope, + context = applicationContext, interactor = screenRecordChipInteractor, endMediaProjectionDialogHelper = endMediaProjectionDialogHelper, dialogTransitionAnimator = mockDialogTransitionAnimator, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt index 535f81a7d63e..2e475a3c6885 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel +import android.content.applicationContext import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope @@ -27,6 +28,7 @@ val Kosmos.shareToAppChipViewModel: ShareToAppChipViewModel by Kosmos.Fixture { ShareToAppChipViewModel( scope = applicationCoroutineScope, + context = applicationContext, mediaProjectionChipInteractor = mediaProjectionChipInteractor, systemClock = fakeSystemClock, endMediaProjectionDialogHelper = endMediaProjectionDialogHelper, diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java index d3efa21a2311..9fc64a965f4b 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java @@ -26,6 +26,8 @@ import android.annotation.MainThread; import android.annotation.NonNull; import android.content.Context; import android.graphics.Region; +import android.hardware.input.InputManager; +import android.os.Looper; import android.os.PowerManager; import android.os.SystemClock; import android.provider.Settings; @@ -54,6 +56,7 @@ import com.android.server.policy.WindowManagerPolicy; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Objects; import java.util.StringJoiner; /** @@ -158,6 +161,13 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo */ static final int FLAG_FEATURE_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP = 0x00001000; + /** + * Flag for enabling the Accessibility mouse key events feature. + * + * @see #setUserAndEnabledFeatures(int, int) + */ + static final int FLAG_FEATURE_MOUSE_KEYS = 0x00002000; + static final int FEATURES_AFFECTING_MOTION_EVENTS = FLAG_FEATURE_INJECT_MOTION_EVENTS | FLAG_FEATURE_AUTOCLICK @@ -189,6 +199,8 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo private KeyboardInterceptor mKeyboardInterceptor; + private MouseKeysInterceptor mMouseKeysInterceptor; + private boolean mInstalled; private int mUserId; @@ -733,6 +745,15 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo // default display. addFirstEventHandler(Display.DEFAULT_DISPLAY, mKeyboardInterceptor); } + + if ((mEnabledFeatures & FLAG_FEATURE_MOUSE_KEYS) != 0) { + mMouseKeysInterceptor = new MouseKeysInterceptor(mAms, + Objects.requireNonNull(mContext.getSystemService( + InputManager.class)), + Looper.myLooper(), + Display.DEFAULT_DISPLAY); + addFirstEventHandler(Display.DEFAULT_DISPLAY, mMouseKeysInterceptor); + } } /** @@ -816,6 +837,11 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo mKeyboardInterceptor.onDestroy(); mKeyboardInterceptor = null; } + + if (mMouseKeysInterceptor != null) { + mMouseKeysInterceptor.onDestroy(); + mMouseKeysInterceptor = null; + } } private MagnificationGestureHandler createMagnificationGestureHandler( diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 32491b7eb0e0..b918d80fc63d 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -57,6 +57,7 @@ import static com.android.internal.accessibility.util.AccessibilityStatsLogUtils import static com.android.internal.util.FunctionalUtils.ignoreRemoteException; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import static com.android.server.accessibility.AccessibilityUserState.doesShortcutTargetsStringContain; +import static com.android.hardware.input.Flags.keyboardA11yMouseKeys; import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import android.accessibilityservice.AccessibilityGestureEvent; @@ -2936,6 +2937,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (combinedGenericMotionEventSources != 0) { flags |= AccessibilityInputFilter.FLAG_FEATURE_INTERCEPT_GENERIC_MOTION_EVENTS; } + if (userState.isMouseKeysEnabled()) { + flags |= AccessibilityInputFilter.FLAG_FEATURE_MOUSE_KEYS; + } if (flags != 0) { if (!mHasInputFilter) { mHasInputFilter = true; @@ -3216,6 +3220,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub somethingChanged |= readMagnificationCapabilitiesLocked(userState); somethingChanged |= readMagnificationFollowTypingLocked(userState); somethingChanged |= readAlwaysOnMagnificationLocked(userState); + somethingChanged |= readMouseKeysEnabledLocked(userState); return somethingChanged; } @@ -5476,6 +5481,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private final Uri mAlwaysOnMagnificationUri = Settings.Secure.getUriFor( Settings.Secure.ACCESSIBILITY_MAGNIFICATION_ALWAYS_ON_ENABLED); + private final Uri mMouseKeysUri = Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ENABLED); + public AccessibilityContentObserver(Handler handler) { super(handler); } @@ -5524,6 +5532,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mMagnificationFollowTypingUri, false, this, UserHandle.USER_ALL); contentResolver.registerContentObserver( mAlwaysOnMagnificationUri, false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver( + mMouseKeysUri, false, this, UserHandle.USER_ALL); } @Override @@ -5604,6 +5614,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub readMagnificationFollowTypingLocked(userState); } else if (mAlwaysOnMagnificationUri.equals(uri)) { readAlwaysOnMagnificationLocked(userState); + } else if (mMouseKeysUri.equals(uri)) { + if (readMouseKeysEnabledLocked(userState)) { + onUserStateChangedLocked(userState); + } } } } @@ -5742,6 +5756,20 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return false; } + boolean readMouseKeysEnabledLocked(AccessibilityUserState userState) { + if (!keyboardA11yMouseKeys()) { + return false; + } + final boolean isMouseKeysEnabled = + Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ENABLED, 0, userState.mUserId) == 1; + if (isMouseKeysEnabled != userState.isMouseKeysEnabled()) { + userState.setMouseKeysEnabled(isMouseKeysEnabled); + return true; + } + return false; + } + @Override public void setGestureDetectionPassthroughRegion(int displayId, Region region) { mMainHandler.sendMessage( diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java index 7bcbc2768a16..b061065d44a5 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java @@ -169,6 +169,8 @@ class AccessibilityUserState { private final int mFocusStrokeWidthDefaultValue; // The default value of the focus color. private final int mFocusColorDefaultValue; + /** Whether mouse keys feature is enabled. */ + private boolean mMouseKeysEnabled = false; private final Map<ComponentName, ComponentName> mA11yServiceToTileService = new ArrayMap<>(); private final Map<ComponentName, ComponentName> mA11yActivityToTileService = new ArrayMap<>(); @@ -674,6 +676,14 @@ class AccessibilityUserState { mIsFilterKeyEventsEnabled = enabled; } + public void setMouseKeysEnabled(boolean enabled) { + mMouseKeysEnabled = enabled; + } + + public boolean isMouseKeysEnabled() { + return mMouseKeysEnabled; + } + public int getInteractiveUiTimeoutLocked() { return mInteractiveUiTimeout; } diff --git a/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java b/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java new file mode 100644 index 000000000000..3f0f23f4a2f9 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java @@ -0,0 +1,498 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.accessibility; + +import static android.accessibilityservice.AccessibilityTrace.FLAGS_INPUT_FILTER; +import static android.util.MathUtils.sqrt; + +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.companion.virtual.VirtualDeviceManager; +import android.companion.virtual.VirtualDeviceParams; +import android.hardware.input.InputManager; +import android.hardware.input.VirtualMouse; +import android.hardware.input.VirtualMouseButtonEvent; +import android.hardware.input.VirtualMouseConfig; +import android.hardware.input.VirtualMouseRelativeEvent; +import android.hardware.input.VirtualMouseScrollEvent; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; +import android.view.KeyEvent; + +import com.android.server.LocalServices; +import com.android.server.companion.virtual.VirtualDeviceManagerInternal; + +/** + * Implements the "mouse keys" accessibility feature for physical keyboards. + * + * If enabled, mouse keys will allow users to use a physical keyboard to + * control the mouse on the display. + * The following mouse functionality is supported by the mouse keys: + * <ul> + * <li> Move the mouse pointer in different directions (up, down, left, right and diagonally). + * <li> Click the mouse button (left, right and middle click). + * <li> Press and hold the mouse button. + * <li> Release the mouse button. + * <li> Scroll (up and down). + * </ul> + * + * The keys that are mapped to mouse keys are consumed by {@link AccessibilityInputFilter}. + * Non-mouse key {@link KeyEvent} will be passed to the parent handler to be handled as usual. + * A new {@link VirtualMouse} is created whenever the mouse keys feature is turned on in Settings. + * In case multiple physical keyboard are connected to a device, + * mouse keys of each physical keyboard will control a single (global) mouse pointer. + */ +public class MouseKeysInterceptor extends BaseEventStreamTransformation implements Handler.Callback, + InputManager.InputDeviceListener { + private static final String LOG_TAG = "MouseKeysInterceptor"; + + // To enable these logs, run: 'adb shell setprop log.tag.MouseKeysInterceptor DEBUG' + // (requires restart) + private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG); + + private static final int MESSAGE_MOVE_MOUSE_POINTER = 1; + private static final int MESSAGE_SCROLL_MOUSE_POINTER = 2; + private static final float MOUSE_POINTER_MOVEMENT_STEP = 1.8f; + private static final int KEY_NOT_SET = -1; + + /** Time interval after which mouse action will be repeated */ + private static final int INTERVAL_MILLIS = 10; + + private final AccessibilityManagerService mAms; + private final InputManager mInputManager; + private final Handler mHandler; + + private final int mDisplayId; + + VirtualDeviceManager.VirtualDevice mVirtualDevice = null; + + private VirtualMouse mVirtualMouse = null; + + /** + * State of the active directional mouse key. + * Multiple mouse keys will not be allowed to be used simultaneously i.e., + * once a mouse key is pressed, other mouse key presses will be disregarded + * (except for when the "HOLD" key is pressed). + */ + private int mActiveMoveKey = KEY_NOT_SET; + + /** State of the active scroll mouse key. */ + private int mActiveScrollKey = KEY_NOT_SET; + + /** Last time the key action was performed */ + private long mLastTimeKeyActionPerformed = 0; + + // TODO (b/346706749): This is currently using the numpad key bindings for mouse keys. + // Decide the final mouse key bindings with UX input. + public enum MouseKeyEvent { + DIAGONAL_DOWN_LEFT_MOVE(KeyEvent.KEYCODE_NUMPAD_1), + DOWN_MOVE(KeyEvent.KEYCODE_NUMPAD_2), + DIAGONAL_DOWN_RIGHT_MOVE(KeyEvent.KEYCODE_NUMPAD_3), + LEFT_MOVE(KeyEvent.KEYCODE_NUMPAD_4), + RIGHT_MOVE(KeyEvent.KEYCODE_NUMPAD_6), + DIAGONAL_UP_LEFT_MOVE(KeyEvent.KEYCODE_NUMPAD_7), + UP_MOVE(KeyEvent.KEYCODE_NUMPAD_8), + DIAGONAL_UP_RIGHT_MOVE(KeyEvent.KEYCODE_NUMPAD_9), + LEFT_CLICK(KeyEvent.KEYCODE_NUMPAD_5), + RIGHT_CLICK(KeyEvent.KEYCODE_NUMPAD_DOT), + HOLD(KeyEvent.KEYCODE_NUMPAD_MULTIPLY), + RELEASE(KeyEvent.KEYCODE_NUMPAD_SUBTRACT), + SCROLL_UP(KeyEvent.KEYCODE_A), + SCROLL_DOWN(KeyEvent.KEYCODE_S); + + private final int mKeyCode; + MouseKeyEvent(int enumValue) { + mKeyCode = enumValue; + } + + private static final SparseArray<MouseKeyEvent> VALUE_TO_ENUM_MAP = new SparseArray<>(); + + static { + for (MouseKeyEvent type : MouseKeyEvent.values()) { + VALUE_TO_ENUM_MAP.put(type.mKeyCode, type); + } + } + + public final int getKeyCodeValue() { + return mKeyCode; + } + + /** + * Convert int value of the key code to corresponding MouseEvent enum. If no matching + * value is found, this will return {@code null}. + */ + @Nullable + public static MouseKeyEvent from(int value) { + return VALUE_TO_ENUM_MAP.get(value); + } + } + + /** + * Construct a new MouseKeysInterceptor. + * + * @param service The service to notify of key events + * @param inputManager InputManager to track changes to connected input devices + * @param looper Looper to use for callbacks and messages + * @param displayId Display ID to send mouse events to + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + public MouseKeysInterceptor(AccessibilityManagerService service, InputManager inputManager, + Looper looper, int displayId) { + mAms = service; + mInputManager = inputManager; + mHandler = new Handler(looper, this); + mInputManager.registerInputDeviceListener(this, mHandler); + mDisplayId = displayId; + // Create the virtual mouse on a separate thread since virtual device creation + // should happen on an auxiliary thread, and not from the handler's thread. + new Thread(() -> { + mVirtualMouse = createVirtualMouse(); + }).start(); + + } + + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + private void sendVirtualMouseRelativeEvent(float x, float y) { + if (mVirtualMouse != null) { + mVirtualMouse.sendRelativeEvent(new VirtualMouseRelativeEvent.Builder() + .setRelativeX(x) + .setRelativeY(y) + .build() + ); + } + } + + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + private void sendVirtualMouseButtonEvent(int buttonCode, int actionCode) { + if (mVirtualMouse != null) { + mVirtualMouse.sendButtonEvent(new VirtualMouseButtonEvent.Builder() + .setAction(actionCode) + .setButtonCode(buttonCode) + .build() + ); + } + } + + /** + * Performs a mouse scroll action based on the provided key code. + * This method interprets the key code as a mouse scroll and sends + * the corresponding {@code VirtualMouseScrollEvent#mYAxisMovement}. + + * @param keyCode The key code representing the mouse scroll action. + * Supported keys are: + * <ul> + * <li>{@link MouseKeysInterceptor.MouseKeyEvent SCROLL_UP} + * <li>{@link MouseKeysInterceptor.MouseKeyEvent SCROLL_DOWN} + * </ul> + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + private void performMouseScrollAction(int keyCode) { + MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from(keyCode); + float y = switch (mouseKeyEvent) { + case SCROLL_UP -> 1.0f; + case SCROLL_DOWN -> -1.0f; + default -> 0.0f; + }; + if (mVirtualMouse != null) { + mVirtualMouse.sendScrollEvent(new VirtualMouseScrollEvent.Builder() + .setYAxisMovement(y) + .build() + ); + } + if (DEBUG) { + Slog.d(LOG_TAG, "Performed mouse key event: " + mouseKeyEvent.name() + + " for scroll action with axis movement (y=" + y + ")"); + } + } + + /** + * Performs a mouse button action based on the provided key code. + * This method interprets the key code as a mouse button press and sends + * the corresponding press and release events to the virtual mouse. + + * @param keyCode The key code representing the mouse button action. + * Supported keys are: + * <ul> + * <li>{@link MouseKeysInterceptor.MouseKeyEvent LEFT_CLICK} (Primary Button) + * <li>{@link MouseKeysInterceptor.MouseKeyEvent RIGHT_CLICK} (Secondary + * Button) + * </ul> + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + private void performMouseButtonAction(int keyCode) { + MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from(keyCode); + int buttonCode = switch (mouseKeyEvent) { + case LEFT_CLICK -> VirtualMouseButtonEvent.BUTTON_PRIMARY; + case RIGHT_CLICK -> VirtualMouseButtonEvent.BUTTON_SECONDARY; + default -> VirtualMouseButtonEvent.BUTTON_UNKNOWN; + }; + if (buttonCode != VirtualMouseButtonEvent.BUTTON_UNKNOWN) { + sendVirtualMouseButtonEvent(buttonCode, VirtualMouseButtonEvent.ACTION_BUTTON_PRESS); + sendVirtualMouseButtonEvent(buttonCode, VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE); + } + if (DEBUG) { + if (buttonCode == VirtualMouseButtonEvent.BUTTON_UNKNOWN) { + Slog.d(LOG_TAG, "Button code is unknown for mouse key event: " + + mouseKeyEvent.name()); + } else { + Slog.d(LOG_TAG, "Performed mouse key event: " + mouseKeyEvent.name() + + " for button action"); + } + } + } + + /** + * Performs a mouse pointer action based on the provided key code. + * The method calculates the relative movement of the mouse pointer + * and sends the corresponding event to the virtual mouse. + * + * @param keyCode The key code representing the direction or button press. + * Supported keys are: + * <ul> + * <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_DOWN_LEFT} + * <li>{@link MouseKeysInterceptor.MouseKeyEvent DOWN} + * <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_DOWN_RIGHT} + * <li>{@link MouseKeysInterceptor.MouseKeyEvent LEFT} + * <li>{@link MouseKeysInterceptor.MouseKeyEvent RIGHT} + * <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_UP_LEFT} + * <li>{@link MouseKeysInterceptor.MouseKeyEvent UP} + * <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_UP_RIGHT} + * </ul> + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + private void performMousePointerAction(int keyCode) { + float x = 0f; + float y = 0f; + MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from(keyCode); + switch (mouseKeyEvent) { + case DIAGONAL_DOWN_LEFT_MOVE -> { + x = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); + y = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); + } + case DOWN_MOVE -> { + y = MOUSE_POINTER_MOVEMENT_STEP; + } + case DIAGONAL_DOWN_RIGHT_MOVE -> { + x = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); + y = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); + } + case LEFT_MOVE -> { + x = -MOUSE_POINTER_MOVEMENT_STEP; + } + case RIGHT_MOVE -> { + x = MOUSE_POINTER_MOVEMENT_STEP; + } + case DIAGONAL_UP_LEFT_MOVE -> { + x = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); + y = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); + } + case UP_MOVE -> { + y = -MOUSE_POINTER_MOVEMENT_STEP; + } + case DIAGONAL_UP_RIGHT_MOVE -> { + x = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); + y = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); + } + default -> { + x = 0.0f; + y = 0.0f; + } + } + sendVirtualMouseRelativeEvent(x, y); + if (DEBUG) { + Slog.d(LOG_TAG, "Performed mouse key event: " + mouseKeyEvent.name() + + " for relative pointer movement (x=" + x + ", y=" + y + ")"); + } + } + + private boolean isMouseKey(int keyCode) { + return MouseKeyEvent.VALUE_TO_ENUM_MAP.contains(keyCode); + } + + private boolean isMouseButtonKey(int keyCode) { + return keyCode == MouseKeyEvent.LEFT_CLICK.getKeyCodeValue() + || keyCode == MouseKeyEvent.RIGHT_CLICK.getKeyCodeValue(); + } + + private boolean isMouseScrollKey(int keyCode) { + return keyCode == MouseKeyEvent.SCROLL_UP.getKeyCodeValue() + || keyCode == MouseKeyEvent.SCROLL_DOWN.getKeyCodeValue(); + } + + /** + * Create a virtual mouse using the VirtualDeviceManagerInternal. + * + * @return The created VirtualMouse. + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + private VirtualMouse createVirtualMouse() { + final VirtualDeviceManagerInternal localVdm = + LocalServices.getService(VirtualDeviceManagerInternal.class); + mVirtualDevice = localVdm.createVirtualDevice( + new VirtualDeviceParams.Builder().setName("Mouse Keys Virtual Device").build()); + VirtualMouse virtualMouse = mVirtualDevice.createVirtualMouse( + new VirtualMouseConfig.Builder() + .setInputDeviceName("Mouse Keys Virtual Mouse") + .setAssociatedDisplayId(mDisplayId) + .build()); + return virtualMouse; + } + + /** + * Handles key events and forwards mouse key events to the virtual mouse. + * + * @param event The key event to handle. + * @param policyFlags The policy flags associated with the key event. + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + @Override + public void onKeyEvent(KeyEvent event, int policyFlags) { + if (mAms.getTraceManager().isA11yTracingEnabledForTypes(FLAGS_INPUT_FILTER)) { + mAms.getTraceManager().logTrace(LOG_TAG + ".onKeyEvent", + FLAGS_INPUT_FILTER, "event=" + event + ";policyFlags=" + policyFlags); + } + boolean isDown = event.getAction() == KeyEvent.ACTION_DOWN; + int keyCode = event.getKeyCode(); + + if (!isMouseKey(keyCode)) { + // Pass non-mouse key events to the next handler + super.onKeyEvent(event, policyFlags); + } else if (keyCode == MouseKeyEvent.HOLD.getKeyCodeValue()) { + sendVirtualMouseButtonEvent(VirtualMouseButtonEvent.BUTTON_PRIMARY, + VirtualMouseButtonEvent.ACTION_BUTTON_PRESS); + } else if (keyCode == MouseKeyEvent.RELEASE.getKeyCodeValue()) { + sendVirtualMouseButtonEvent(VirtualMouseButtonEvent.BUTTON_PRIMARY, + VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE); + } else if (isDown && isMouseButtonKey(keyCode)) { + performMouseButtonAction(keyCode); + } else if (isDown && isMouseScrollKey(keyCode)) { + // If the scroll key is pressed down and no other key is active, + // set it as the active key and send a message to scroll the pointer + if (mActiveScrollKey == KEY_NOT_SET) { + mActiveScrollKey = keyCode; + mLastTimeKeyActionPerformed = event.getDownTime(); + mHandler.sendEmptyMessage(MESSAGE_SCROLL_MOUSE_POINTER); + } + } else if (isDown) { + // This is a directional key. + // If the key is pressed down and no other key is active, + // set it as the active key and send a message to move the pointer + if (mActiveMoveKey == KEY_NOT_SET) { + mActiveMoveKey = keyCode; + mLastTimeKeyActionPerformed = event.getDownTime(); + mHandler.sendEmptyMessage(MESSAGE_MOVE_MOUSE_POINTER); + } + } else if (mActiveMoveKey == keyCode) { + // If the key is released, and it is the active key, stop moving the pointer + mActiveMoveKey = KEY_NOT_SET; + mHandler.removeMessages(MESSAGE_MOVE_MOUSE_POINTER); + } else if (mActiveScrollKey == keyCode) { + // If the key is released, and it is the active key, stop scrolling the pointer + mActiveScrollKey = KEY_NOT_SET; + mHandler.removeMessages(MESSAGE_SCROLL_MOUSE_POINTER); + } else { + Slog.i(LOG_TAG, "Dropping event with key code: '" + keyCode + + "', with no matching down event from deviceId = " + event.getDeviceId()); + } + } + + /** + * Handle messages for moving or scrolling the mouse pointer. + * + * @param msg The message to handle. + * @return True if the message was handled, false otherwise. + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_MOVE_MOUSE_POINTER -> + handleMouseMessage(msg.getWhen(), mActiveMoveKey, MESSAGE_MOVE_MOUSE_POINTER); + case MESSAGE_SCROLL_MOUSE_POINTER -> + handleMouseMessage(msg.getWhen(), mActiveScrollKey, + MESSAGE_SCROLL_MOUSE_POINTER); + default -> { + Slog.e(LOG_TAG, "Unexpected message type"); + return false; + } + } + return true; + } + + /** + * Handles mouse-related messages for moving or scrolling the mouse pointer. + * This method checks if the specified time interval {@code INTERVAL_MILLIS} has passed since + * the last movement or scroll action and performs the corresponding action if necessary. + * If there is an active key, the message is rescheduled to be handled again + * after the specified {@code INTERVAL_MILLIS}. + * + * @param currentTime The current time when the message is being handled. + * @param activeKey The key code representing the active key. This determines + * the direction or type of action to be performed. + * @param messageType The type of message to be handled. It can be one of the + * following: + * <ul> + * <li>{@link #MESSAGE_MOVE_MOUSE_POINTER} - for moving the mouse pointer. + * <li>{@link #MESSAGE_SCROLL_MOUSE_POINTER} - for scrolling mouse pointer. + * </ul> + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + public void handleMouseMessage(long currentTime, int activeKey, int messageType) { + if (currentTime - mLastTimeKeyActionPerformed >= INTERVAL_MILLIS) { + if (messageType == MESSAGE_MOVE_MOUSE_POINTER) { + performMousePointerAction(activeKey); + } else if (messageType == MESSAGE_SCROLL_MOUSE_POINTER) { + performMouseScrollAction(activeKey); + } + mLastTimeKeyActionPerformed = currentTime; + } + if (activeKey != KEY_NOT_SET) { + // Reschedule the message if the key is still active + mHandler.sendEmptyMessageDelayed(messageType, INTERVAL_MILLIS); + } + } + + @Override + public void onInputDeviceAdded(int deviceId) { + } + + @Override + public void onInputDeviceRemoved(int deviceId) { + } + + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + @Override + public void onDestroy() { + // Clear mouse state + mActiveMoveKey = KEY_NOT_SET; + mActiveScrollKey = KEY_NOT_SET; + mLastTimeKeyActionPerformed = 0; + mHandler.removeCallbacksAndMessages(null); + + mVirtualDevice.close(); + mInputManager.unregisterInputDeviceListener(this); + } + + @Override + public void onInputDeviceChanged(int deviceId) { + } + +} diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index bacfd8f9960e..3633d0f9dd6f 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -2989,7 +2989,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { // Always redact location info from PhysicalChannelConfig if the registrant is from neither // PHONE nor SYSTEM process. There is no user case that the registrant needs the location // info (e.g. physicalCellId). This also remove the need for the location permissions check. - return record.callerUid != Process.PHONE_UID && record.callerUid != Process.SYSTEM_UID; + return !TelephonyPermissions.isSystemOrPhone(record.callerUid); } /** diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 6d9b4f547be6..0d309eb6786d 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -977,7 +977,7 @@ public class AudioService extends IAudioService.Stub private NotificationManager mNm; private AudioManagerInternal.RingerModeDelegate mRingerModeDelegate; - private VolumePolicy mVolumePolicy = VolumePolicy.DEFAULT; + private volatile VolumePolicy mVolumePolicy = VolumePolicy.DEFAULT; private long mLoweredFromNormalToVibrateTime; // Array of Uids of valid assistant services to check if caller is one of them @@ -12101,6 +12101,11 @@ public class AudioService extends IAudioService.Stub } } + @Override + public VolumePolicy getVolumePolicy() { + return mVolumePolicy; + } + /** Interface used for enforcing the safe hearing standard. */ public interface ISafeHearingVolumeController { /** Displays an instructional safeguard as required by the safe hearing standard. */ diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java index eb78fe620c0b..a1184156f86e 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java @@ -20,7 +20,6 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.hardware.biometrics.BiometricConstants; -import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; @@ -28,11 +27,12 @@ import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; +import com.android.modules.expresslog.Counter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; -import java.util.function.BooleanSupplier; + /** * Contains all the necessary information for a HAL operation. @@ -89,8 +89,6 @@ public class BiometricSchedulerOperation { private final BaseClientMonitor mClientMonitor; @Nullable private final ClientMonitorCallback mClientCallback; - @NonNull - private final BooleanSupplier mIsDebuggable; @Nullable private ClientMonitorCallback mOnStartCallback; @OperationState @@ -99,6 +97,7 @@ public class BiometricSchedulerOperation { @NonNull final Runnable mCancelWatchdog; + @VisibleForTesting BiometricSchedulerOperation( @NonNull BaseClientMonitor clientMonitor, @Nullable ClientMonitorCallback callback @@ -106,33 +105,14 @@ public class BiometricSchedulerOperation { this(clientMonitor, callback, STATE_WAITING_IN_QUEUE); } - @VisibleForTesting - BiometricSchedulerOperation( - @NonNull BaseClientMonitor clientMonitor, - @Nullable ClientMonitorCallback callback, - @NonNull BooleanSupplier isDebuggable - ) { - this(clientMonitor, callback, STATE_WAITING_IN_QUEUE, isDebuggable); - } - protected BiometricSchedulerOperation( @NonNull BaseClientMonitor clientMonitor, @Nullable ClientMonitorCallback callback, @OperationState int state ) { - this(clientMonitor, callback, state, Build::isDebuggable); - } - - private BiometricSchedulerOperation( - @NonNull BaseClientMonitor clientMonitor, - @Nullable ClientMonitorCallback callback, - @OperationState int state, - @NonNull BooleanSupplier isDebuggable - ) { mClientMonitor = clientMonitor; mClientCallback = callback; mState = state; - mIsDebuggable = isDebuggable; mCancelWatchdog = () -> { if (!isFinished()) { Slog.e(TAG, "[Watchdog Triggered]: " + this); @@ -187,9 +167,7 @@ public class BiometricSchedulerOperation { if (mClientMonitor.getCookie() != 0) { String err = "operation requires cookie"; - if (mIsDebuggable.getAsBoolean()) { - throw new IllegalStateException(err); - } + Counter.logIncrement("biometric.value_biometric_scheduler_operation_state_error_count"); Slog.e(TAG, err); } @@ -456,10 +434,9 @@ public class BiometricSchedulerOperation { private boolean errorWhenOneOf(String op, @OperationState int... states) { final boolean isError = ArrayUtils.contains(states, mState); if (isError) { - String err = op + ": mState must not be " + mState; - if (mIsDebuggable.getAsBoolean()) { - throw new IllegalStateException(err); - } + Counter.logIncrement( + "biometric.value_biometric_scheduler_operation_state_error_count"); + final String err = op + ": mState must not be " + mState; Slog.e(TAG, err); } return isError; @@ -468,10 +445,10 @@ public class BiometricSchedulerOperation { private boolean errorWhenNoneOf(String op, @OperationState int... states) { final boolean isError = !ArrayUtils.contains(states, mState); if (isError) { - String err = op + ": mState=" + mState + " must be one of " + Arrays.toString(states); - if (mIsDebuggable.getAsBoolean()) { - throw new IllegalStateException(err); - } + Counter.logIncrement( + "biometric.value_biometric_scheduler_operation_state_error_count"); + final String err = op + ": mState=" + mState + " must be one of " + + Arrays.toString(states); Slog.e(TAG, err); } return isError; diff --git a/services/core/java/com/android/server/location/altitude/AltitudeService.java b/services/core/java/com/android/server/location/altitude/AltitudeService.java index 289d4a25f208..96540c225e23 100644 --- a/services/core/java/com/android/server/location/altitude/AltitudeService.java +++ b/services/core/java/com/android/server/location/altitude/AltitudeService.java @@ -25,6 +25,7 @@ import android.frameworks.location.altitude.IAltitudeService; import android.location.Location; import android.location.altitude.AltitudeConverter; import android.os.RemoteException; +import android.util.Log; import com.android.server.SystemService; @@ -38,6 +39,8 @@ import java.io.IOException; */ public class AltitudeService extends IAltitudeService.Stub { + private static final String TAG = "AltitudeService"; + private final AltitudeConverter mAltitudeConverter = new AltitudeConverter(); private final Context mContext; @@ -59,6 +62,7 @@ public class AltitudeService extends IAltitudeService.Stub { try { mAltitudeConverter.addMslAltitudeToLocation(mContext, location); } catch (IOException e) { + Log.e(TAG, "", e); response.success = false; return response; } @@ -74,6 +78,7 @@ public class AltitudeService extends IAltitudeService.Stub { try { return mAltitudeConverter.getGeoidHeight(mContext, request); } catch (IOException e) { + Log.e(TAG, "", e); GetGeoidHeightResponse response = new GetGeoidHeightResponse(); response.success = false; return response; diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 1c40f44b7b78..9e53cc357ea4 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -8658,8 +8658,7 @@ public class NotificationManagerService extends SystemService { mAttentionHelper.updateLightsLocked(); if (mShortcutHelper != null) { mShortcutHelper.maybeListenForShortcutChangesForBubbles(r, - true /* isRemoved */, - mHandler); + true /* isRemoved */); } } else { if (notificationForceGrouping()) { @@ -9116,8 +9115,7 @@ public class NotificationManagerService extends SystemService { if (mShortcutHelper != null) { mShortcutHelper.maybeListenForShortcutChangesForBubbles(r, - false /* isRemoved */, - mHandler); + false /* isRemoved */); } maybeRecordInterruptionLocked(r); diff --git a/services/core/java/com/android/server/notification/ShortcutHelper.java b/services/core/java/com/android/server/notification/ShortcutHelper.java index 86dcecf9290a..857e3198d55b 100644 --- a/services/core/java/com/android/server/notification/ShortcutHelper.java +++ b/services/core/java/com/android/server/notification/ShortcutHelper.java @@ -27,7 +27,6 @@ import android.content.pm.LauncherApps; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutServiceInternal; import android.os.Binder; -import android.os.Handler; import android.os.UserHandle; import android.os.UserManager; import android.text.TextUtils; @@ -65,85 +64,34 @@ public class ShortcutHelper { void onShortcutRemoved(String key); } + private final ShortcutListener mShortcutListener; private LauncherApps mLauncherAppsService; - private ShortcutListener mShortcutListener; private ShortcutServiceInternal mShortcutServiceInternal; private UserManager mUserManager; - // Key: packageName Value: <shortcutId, notifId> - private HashMap<String, HashMap<String, String>> mActiveShortcutBubbles = new HashMap<>(); - private boolean mLauncherAppsCallbackRegistered; + // Key: packageName|userId Value: <shortcutId, notifId> + private final HashMap<String, HashMap<String, String>> mActiveShortcutBubbles = new HashMap<>(); + private boolean mShortcutChangedCallbackRegistered; // Bubbles can be created based on a shortcut, we need to listen for changes to // that shortcut so that we may update the bubble appropriately. - private final LauncherApps.Callback mLauncherAppsCallback = new LauncherApps.Callback() { - @Override - public void onPackageRemoved(String packageName, UserHandle user) { - } - - @Override - public void onPackageAdded(String packageName, UserHandle user) { - } - - @Override - public void onPackageChanged(String packageName, UserHandle user) { - } - - @Override - public void onPackagesAvailable(String[] packageNames, UserHandle user, - boolean replacing) { - } - - @Override - public void onPackagesUnavailable(String[] packageNames, UserHandle user, - boolean replacing) { - } + private final LauncherApps.ShortcutChangeCallback mShortcutChangeCallback = + new LauncherApps.ShortcutChangeCallback() { - @Override - public void onShortcutsChanged(@NonNull String packageName, - @NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user) { - HashMap<String, String> shortcutBubbles = mActiveShortcutBubbles.get(packageName); - ArrayList<String> bubbleKeysToRemove = new ArrayList<>(); - if (shortcutBubbles != null) { - // Copy to avoid a concurrent modification exception when we remove bubbles from - // shortcutBubbles. - final Set<String> shortcutIds = new HashSet<>(shortcutBubbles.keySet()); - - // If we can't find one of our bubbles in the shortcut list, that bubble needs - // to be removed. - for (String shortcutId : shortcutIds) { - boolean foundShortcut = false; - for (int i = 0; i < shortcuts.size(); i++) { - if (shortcuts.get(i).getId().equals(shortcutId)) { - foundShortcut = true; - break; - } - } - if (!foundShortcut) { - bubbleKeysToRemove.add(shortcutBubbles.get(shortcutId)); - shortcutBubbles.remove(shortcutId); - if (shortcutBubbles.isEmpty()) { - mActiveShortcutBubbles.remove(packageName); - if (mLauncherAppsCallbackRegistered - && mActiveShortcutBubbles.isEmpty()) { - mLauncherAppsService.unregisterCallback(mLauncherAppsCallback); - mLauncherAppsCallbackRegistered = false; - } - } - } + @Override + public void onShortcutsAddedOrUpdated(@NonNull String packageName, + @NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user) { } - } - // Let NoMan know about the updates - for (int i = 0; i < bubbleKeysToRemove.size(); i++) { - // update flag bubble - String bubbleKey = bubbleKeysToRemove.get(i); - if (mShortcutListener != null) { - mShortcutListener.onShortcutRemoved(bubbleKey); + public void onShortcutsRemoved(@NonNull String packageName, + @NonNull List<ShortcutInfo> removedShortcuts, @NonNull UserHandle user) { + final String packageUserKey = getPackageUserKey(packageName, user); + if (mActiveShortcutBubbles.get(packageUserKey) == null) return; + for (ShortcutInfo info : removedShortcuts) { + onShortcutRemoved(packageUserKey, info.getId()); + } } - } - } - }; + }; ShortcutHelper(LauncherApps launcherApps, ShortcutListener listener, ShortcutServiceInternal shortcutServiceInternal, UserManager userManager) { @@ -172,14 +120,14 @@ public class ShortcutHelper { * Returns whether the given shortcut info is a conversation shortcut. */ public static boolean isConversationShortcut( - ShortcutInfo shortcutInfo, ShortcutServiceInternal mShortcutServiceInternal, + ShortcutInfo shortcutInfo, ShortcutServiceInternal shortcutServiceInternal, int callingUserId) { if (shortcutInfo == null || !shortcutInfo.isLongLived() || !shortcutInfo.isEnabled()) { return false; } // TODO (b/155016294) uncomment when sharing shortcuts are required /* - mShortcutServiceInternal.isSharingShortcut(callingUserId, "android", + shortcutServiceInternal.isSharingShortcut(callingUserId, "android", shortcutInfo.getPackage(), shortcutInfo.getId(), shortcutInfo.getUserId(), SHARING_FILTER); */ @@ -233,34 +181,30 @@ public class ShortcutHelper { * * @param r the notification record to check * @param removedNotification true if this notification is being removed - * @param handler handler to register the callback with */ void maybeListenForShortcutChangesForBubbles(NotificationRecord r, - boolean removedNotification, - Handler handler) { + boolean removedNotification) { final String shortcutId = r.getNotification().getBubbleMetadata() != null ? r.getNotification().getBubbleMetadata().getShortcutId() : null; + final String packageUserKey = getPackageUserKey(r.getSbn().getPackageName(), r.getUser()); if (!removedNotification && !TextUtils.isEmpty(shortcutId) && r.getShortcutInfo() != null && r.getShortcutInfo().getId().equals(shortcutId)) { // Must track shortcut based bubbles in case the shortcut is removed HashMap<String, String> packageBubbles = mActiveShortcutBubbles.get( - r.getSbn().getPackageName()); + packageUserKey); if (packageBubbles == null) { packageBubbles = new HashMap<>(); } packageBubbles.put(shortcutId, r.getKey()); - mActiveShortcutBubbles.put(r.getSbn().getPackageName(), packageBubbles); - if (!mLauncherAppsCallbackRegistered) { - mLauncherAppsService.registerCallback(mLauncherAppsCallback, handler); - mLauncherAppsCallbackRegistered = true; - } + mActiveShortcutBubbles.put(packageUserKey, packageBubbles); + registerCallbackIfNeeded(); } else { // No longer track shortcut HashMap<String, String> packageBubbles = mActiveShortcutBubbles.get( - r.getSbn().getPackageName()); + packageUserKey); if (packageBubbles != null) { if (!TextUtils.isEmpty(shortcutId)) { packageBubbles.remove(shortcutId); @@ -278,20 +222,62 @@ public class ShortcutHelper { } } if (packageBubbles.isEmpty()) { - mActiveShortcutBubbles.remove(r.getSbn().getPackageName()); + mActiveShortcutBubbles.remove(packageUserKey); } } - if (mLauncherAppsCallbackRegistered && mActiveShortcutBubbles.isEmpty()) { - mLauncherAppsService.unregisterCallback(mLauncherAppsCallback); - mLauncherAppsCallbackRegistered = false; + unregisterCallbackIfNeeded(); + } + } + + private String getPackageUserKey(String packageName, UserHandle user) { + return packageName + "|" + user.getIdentifier(); + } + + private void onShortcutRemoved(String packageUserKey, String shortcutId) { + HashMap<String, String> shortcutBubbles = mActiveShortcutBubbles.get(packageUserKey); + ArrayList<String> bubbleKeysToRemove = new ArrayList<>(); + if (shortcutBubbles != null) { + if (shortcutBubbles.containsKey(shortcutId)) { + bubbleKeysToRemove.add(shortcutBubbles.get(shortcutId)); + shortcutBubbles.remove(shortcutId); + if (shortcutBubbles.isEmpty()) { + mActiveShortcutBubbles.remove(packageUserKey); + unregisterCallbackIfNeeded(); + } } + notifyNoMan(bubbleKeysToRemove); + } + } + + private void registerCallbackIfNeeded() { + if (!mShortcutChangedCallbackRegistered) { + mShortcutChangedCallbackRegistered = true; + mShortcutServiceInternal.addShortcutChangeCallback(mShortcutChangeCallback); + } + } + + private void unregisterCallbackIfNeeded() { + if (mShortcutChangedCallbackRegistered && mActiveShortcutBubbles.isEmpty()) { + mShortcutServiceInternal.removeShortcutChangeCallback(mShortcutChangeCallback); + mShortcutChangedCallbackRegistered = false; } } void destroy() { - if (mLauncherAppsCallbackRegistered) { - mLauncherAppsService.unregisterCallback(mLauncherAppsCallback); - mLauncherAppsCallbackRegistered = false; + if (mShortcutChangedCallbackRegistered) { + mShortcutServiceInternal.removeShortcutChangeCallback(mShortcutChangeCallback); + mShortcutChangedCallbackRegistered = false; + } + } + + private void notifyNoMan(List<String> bubbleKeysToRemove) { + // Let NoMan know about the updates + for (int i = 0; i < bubbleKeysToRemove.size(); i++) { + // update flag bubble + String bubbleKey = bubbleKeysToRemove.get(i); + if (mShortcutListener != null) { + mShortcutListener.onShortcutRemoved(bubbleKey); + } } } } diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 1cd77ffcedaa..4a60e452919f 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -3443,6 +3443,14 @@ public class ShortcutService extends IShortcutService.Stub { } @Override + public void removeShortcutChangeCallback( + @NonNull LauncherApps.ShortcutChangeCallback callback) { + synchronized (mServiceLock) { + mShortcutChangeCallbacks.remove(Objects.requireNonNull(callback)); + } + } + + @Override public int getShortcutIconResId(int launcherUserId, @NonNull String callingPackage, @NonNull String packageName, @NonNull String shortcutId, int userId) { Objects.requireNonNull(callingPackage, "callingPackage"); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index c9ba683a698a..9d0c0e9b36bb 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -3568,24 +3568,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { Slog.wtf(TAG, "KEYCODE_VOICE_ASSIST should be handled in" + " interceptKeyBeforeQueueing"); return true; - case KeyEvent.KEYCODE_VIDEO_APP_1: - case KeyEvent.KEYCODE_VIDEO_APP_2: - case KeyEvent.KEYCODE_VIDEO_APP_3: - case KeyEvent.KEYCODE_VIDEO_APP_4: - case KeyEvent.KEYCODE_VIDEO_APP_5: - case KeyEvent.KEYCODE_VIDEO_APP_6: - case KeyEvent.KEYCODE_VIDEO_APP_7: - case KeyEvent.KEYCODE_VIDEO_APP_8: - case KeyEvent.KEYCODE_FEATURED_APP_1: - case KeyEvent.KEYCODE_FEATURED_APP_2: - case KeyEvent.KEYCODE_FEATURED_APP_3: - case KeyEvent.KEYCODE_FEATURED_APP_4: - case KeyEvent.KEYCODE_DEMO_APP_1: - case KeyEvent.KEYCODE_DEMO_APP_2: - case KeyEvent.KEYCODE_DEMO_APP_3: - case KeyEvent.KEYCODE_DEMO_APP_4: - Slog.wtf(TAG, "KEYCODE_APP_X should be handled in interceptKeyBeforeQueueing"); - return true; case KeyEvent.KEYCODE_BRIGHTNESS_UP: case KeyEvent.KEYCODE_BRIGHTNESS_DOWN: if (down) { diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index f5757dc7a4de..e81b440f6d6d 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -38,6 +38,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.content.Intent.ACTION_VIEW; import static android.content.pm.PackageManager.NOTIFY_PACKAGE_USE_ACTIVITY; import static android.content.pm.PackageManager.PERMISSION_DENIED; import static android.content.pm.PackageManager.PERMISSION_GRANTED; @@ -121,6 +122,7 @@ import android.content.res.Configuration; import android.graphics.Rect; import android.hardware.SensorPrivacyManager; import android.hardware.SensorPrivacyManagerInternal; +import android.net.Uri; import android.os.Binder; import android.os.Build; import android.os.Bundle; @@ -142,6 +144,7 @@ import android.util.Slog; import android.util.SparseArray; import android.util.SparseIntArray; import android.view.Display; +import android.webkit.URLUtil; import android.window.ActivityWindowInfo; import com.android.internal.R; @@ -158,6 +161,7 @@ import com.android.server.am.UserState; import com.android.server.pm.SaferIntentUtils; import com.android.server.utils.Slogf; import com.android.server.wm.ActivityMetricsLogger.LaunchingState; +import com.android.window.flags.Flags; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -2900,6 +2904,9 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { @Override public void accept(ActivityRecord r) { + if (Flags.enableDesktopWindowingAppToWeb() && mInfo.capturedLink == null) { + setCapturedLink(r); + } if (r.mLaunchCookie != null) { mInfo.addLaunchCookie(r.mLaunchCookie); } @@ -2912,6 +2919,16 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { mTopRunning = r; } } + + private void setCapturedLink(ActivityRecord r) { + final Uri uri = r.intent.getData(); + if (uri == null || !ACTION_VIEW.equals(r.intent.getAction()) + || !URLUtil.isNetworkUrl(uri.toString())) { + return; + } + mInfo.capturedLink = uri; + mInfo.capturedLinkTimestamp = r.lastLaunchTime; + } } /** diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 9a5f84cf7449..803312214fc3 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -9299,7 +9299,8 @@ public class WindowManagerService extends IWindowManager.Stub isTrustedOverlay); final int sanitizedLpFlags = - (flags & (FLAG_NOT_TOUCHABLE | FLAG_SLIPPERY | LayoutParams.FLAG_NOT_FOCUSABLE)) + (flags & (FLAG_NOT_TOUCHABLE | FLAG_SLIPPERY | LayoutParams.FLAG_NOT_FOCUSABLE + | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH)) | LayoutParams.FLAG_NOT_TOUCH_MODAL; h.layoutParamsType = type; h.layoutParamsFlags = sanitizedLpFlags; diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java index f83144f176d3..7c8f11908a36 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java @@ -49,6 +49,7 @@ import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; +import android.util.ArraySet; import android.view.InputChannel; import android.view.inputmethod.EditorInfo; import android.window.ImeOnBackInvokedDispatcher; @@ -134,6 +135,14 @@ public class InputMethodManagerServiceTestBase { protected boolean mIsLargeScreen; private InputManagerGlobal.TestSession mInputManagerGlobalSession; + private final ArraySet<Class<?>> mRegisteredLocalServices = new ArraySet<>(); + + protected <T> void addLocalServiceMock(Class<T> type, T service) { + mRegisteredLocalServices.add(type); + LocalServices.removeServiceForTest(type); + LocalServices.addService(type, service); + } + @BeforeClass public static void setupClass() { // Make sure DeviceConfig's lazy-initialized ContentProvider gets @@ -148,7 +157,6 @@ public class InputMethodManagerServiceTestBase { mockitoSession() .initMocks(this) .strictness(Strictness.LENIENT) - .spyStatic(LocalServices.class) .mockStatic(ServiceManager.class) .mockStatic(SystemServerInitThreadPool.class) .startMocking(); @@ -163,18 +171,13 @@ public class InputMethodManagerServiceTestBase { mEditorInfo.packageName = TEST_EDITOR_PKG_NAME; // Injecting and mocking local services. - doReturn(mMockWindowManagerInternal) - .when(() -> LocalServices.getService(WindowManagerInternal.class)); - doReturn(mMockActivityManagerInternal) - .when(() -> LocalServices.getService(ActivityManagerInternal.class)); - doReturn(mMockPackageManagerInternal) - .when(() -> LocalServices.getService(PackageManagerInternal.class)); - doReturn(mMockInputManagerInternal) - .when(() -> LocalServices.getService(InputManagerInternal.class)); - doReturn(mMockUserManagerInternal) - .when(() -> LocalServices.getService(UserManagerInternal.class)); - doReturn(mMockImeTargetVisibilityPolicy) - .when(() -> LocalServices.getService(ImeTargetVisibilityPolicy.class)); + addLocalServiceMock(WindowManagerInternal.class, mMockWindowManagerInternal); + addLocalServiceMock(ActivityManagerInternal.class, mMockActivityManagerInternal); + addLocalServiceMock(PackageManagerInternal.class, mMockPackageManagerInternal); + addLocalServiceMock(InputManagerInternal.class, mMockInputManagerInternal); + addLocalServiceMock(UserManagerInternal.class, mMockUserManagerInternal); + addLocalServiceMock(ImeTargetVisibilityPolicy.class, mMockImeTargetVisibilityPolicy); + doReturn(mMockIInputMethodManager) .when(() -> ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE)); doReturn(mMockIPlatformCompat) @@ -289,7 +292,7 @@ public class InputMethodManagerServiceTestBase { if (mInputManagerGlobalSession != null) { mInputManagerGlobalSession.close(); } - LocalServices.removeServiceForTest(InputMethodManagerInternal.class); + mRegisteredLocalServices.forEach(LocalServices::removeServiceForTest); } protected void verifyShowSoftInput(boolean setVisible, boolean showSoftInput) diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java index 9f46d0ba7df6..ffc4df8f2069 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java @@ -50,7 +50,6 @@ import com.android.internal.inputmethod.InputBindResult; import com.android.internal.inputmethod.InputMethodDebug; import com.android.internal.inputmethod.StartInputFlags; import com.android.internal.inputmethod.StartInputReason; -import com.android.server.LocalServices; import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import com.android.server.wm.WindowManagerInternal; @@ -270,8 +269,7 @@ public class InputMethodManagerServiceWindowGainedFocusTest @Test public void startInputOrWindowGainedFocus_localeHintsOverride() throws RemoteException { - doReturn(mMockVdmInternal).when( - () -> LocalServices.getService(VirtualDeviceManagerInternal.class)); + addLocalServiceMock(VirtualDeviceManagerInternal.class, mMockVdmInternal); LocaleList overrideLocale = LocaleList.forLanguageTags("zh-CN"); doReturn(overrideLocale).when(mMockVdmInternal).getPreferredLocaleListForUid(anyInt()); mockHasImeFocusAndRestoreImeVisibility(false /* restoreImeVisibility */); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt new file mode 100644 index 000000000000..dc8d2390ef2d --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt @@ -0,0 +1,272 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.accessibility + +import android.companion.virtual.VirtualDeviceManager +import android.companion.virtual.VirtualDeviceParams +import android.content.Context +import android.hardware.input.IInputManager +import android.hardware.input.InputManager +import android.hardware.input.InputManagerGlobal +import android.hardware.input.VirtualMouse +import android.hardware.input.VirtualMouseButtonEvent +import android.hardware.input.VirtualMouseConfig +import android.hardware.input.VirtualMouseRelativeEvent +import android.hardware.input.VirtualMouseScrollEvent +import android.os.RemoteException +import android.os.test.TestLooper +import android.platform.test.annotations.Presubmit +import android.view.KeyEvent +import androidx.test.core.app.ApplicationProvider +import com.android.server.companion.virtual.VirtualDeviceManagerInternal +import com.android.server.LocalServices +import com.android.server.testutils.OffsettableClock +import junit.framework.Assert.assertEquals +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import java.util.concurrent.TimeUnit +import java.util.LinkedList +import java.util.Queue +import android.util.ArraySet + +/** + * Tests for {@link MouseKeysInterceptor} + * + * Build/Install/Run: + * atest FrameworksServicesTests:MouseKeysInterceptorTest + */ +@Presubmit +class MouseKeysInterceptorTest { + companion object { + const val DISPLAY_ID = 1 + const val DEVICE_ID = 123 + // This delay is required for key events to be sent and handled correctly. + // The handler only performs a move/scroll event if it receives the key event + // at INTERVAL_MILLIS (which happens in practice). Hence, we need this delay in the tests. + const val KEYBOARD_POST_EVENT_DELAY_MILLIS = 20L + } + + private lateinit var mouseKeysInterceptor: MouseKeysInterceptor + private val clock = OffsettableClock() + private val testLooper = TestLooper { clock.now() } + private val nextInterceptor = TrackingInterceptor() + + @Mock + private lateinit var mockAms: AccessibilityManagerService + + @Mock + private lateinit var iInputManager: IInputManager + private lateinit var testSession: InputManagerGlobal.TestSession + private lateinit var mockInputManager: InputManager + + @Mock + private lateinit var mockVirtualDeviceManagerInternal: VirtualDeviceManagerInternal + @Mock + private lateinit var mockVirtualDevice: VirtualDeviceManager.VirtualDevice + @Mock + private lateinit var mockVirtualMouse: VirtualMouse + + @Mock + private lateinit var mockTraceManager: AccessibilityTraceManager + + @Before + @Throws(RemoteException::class) + fun setUp() { + MockitoAnnotations.initMocks(this) + val context = ApplicationProvider.getApplicationContext<Context>() + testSession = InputManagerGlobal.createTestSession(iInputManager) + mockInputManager = InputManager(context) + + Mockito.`when`(mockVirtualDeviceManagerInternal.getDeviceIdsForUid(Mockito.anyInt())) + .thenReturn(ArraySet(setOf(DEVICE_ID))) + LocalServices.removeServiceForTest(VirtualDeviceManagerInternal::class.java) + LocalServices.addService<VirtualDeviceManagerInternal>( + VirtualDeviceManagerInternal::class.java, mockVirtualDeviceManagerInternal + ) + + Mockito.`when`(mockVirtualDeviceManagerInternal.createVirtualDevice( + Mockito.any(VirtualDeviceParams::class.java) + )).thenReturn(mockVirtualDevice) + Mockito.`when`(mockVirtualDevice.createVirtualMouse( + Mockito.any(VirtualMouseConfig::class.java) + )).thenReturn(mockVirtualMouse) + + Mockito.`when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID)) + Mockito.`when`(mockAms.traceManager).thenReturn(mockTraceManager) + + mouseKeysInterceptor = MouseKeysInterceptor(mockAms, mockInputManager, + testLooper.looper, DISPLAY_ID) + // VirtualMouse is created on a separate thread. + // Wait for VirtualMouse to be created before running tests + TimeUnit.MILLISECONDS.sleep(20L) + mouseKeysInterceptor.next = nextInterceptor + } + + @After + fun tearDown() { + testLooper.dispatchAll() + if (this::testSession.isInitialized) { + testSession.close() + } + } + + @Test + fun whenNonMouseKeyEventArrives_eventIsPassedToNextInterceptor() { + val downTime = clock.now() + val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_Q, 0, 0, DEVICE_ID, 0) + mouseKeysInterceptor.onKeyEvent(downEvent, 0) + testLooper.dispatchAll() + + assertEquals(1, nextInterceptor.events.size) + assertEquals(downEvent, nextInterceptor.events.poll()) + } + + @Test + fun whenMouseDirectionalKeyIsPressed_relativeEventIsSent() { + // There should be some delay between the downTime of the key event and calling onKeyEvent + val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS + val keyCode = MouseKeysInterceptor.MouseKeyEvent.DOWN_MOVE.getKeyCodeValue() + val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, + keyCode, 0, 0, DEVICE_ID, 0) + + mouseKeysInterceptor.onKeyEvent(downEvent, 0) + testLooper.dispatchAll() + + // Verify the sendRelativeEvent method is called once and capture the arguments + verifyRelativeEvents(arrayOf<Float>(0f), arrayOf<Float>(1.8f)) + } + + @Test + fun whenClickKeyIsPressed_buttonEventIsSent() { + // There should be some delay between the downTime of the key event and calling onKeyEvent + val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS + val keyCode = MouseKeysInterceptor.MouseKeyEvent.LEFT_CLICK.getKeyCodeValue() + val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, + keyCode, 0, 0, DEVICE_ID, 0) + mouseKeysInterceptor.onKeyEvent(downEvent, 0) + testLooper.dispatchAll() + + val actions = arrayOf<Int>( + VirtualMouseButtonEvent.ACTION_BUTTON_PRESS, + VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE) + val buttons = arrayOf<Int>( + VirtualMouseButtonEvent.BUTTON_PRIMARY, + VirtualMouseButtonEvent.BUTTON_PRIMARY) + // Verify the sendButtonEvent method is called twice and capture the arguments + verifyButtonEvents(actions, buttons) + } + + @Test + fun whenHoldKeyIsPressed_buttonEventIsSent() { + val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS + val keyCode = MouseKeysInterceptor.MouseKeyEvent.HOLD.getKeyCodeValue() + val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, + keyCode, 0, 0, DEVICE_ID, 0) + mouseKeysInterceptor.onKeyEvent(downEvent, 0) + testLooper.dispatchAll() + + // Verify the sendButtonEvent method is called once and capture the arguments + verifyButtonEvents( + arrayOf<Int>( VirtualMouseButtonEvent.ACTION_BUTTON_PRESS), + arrayOf<Int>( VirtualMouseButtonEvent.BUTTON_PRIMARY) + ) + } + + @Test + fun whenReleaseKeyIsPressed_buttonEventIsSent() { + val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS + val keyCode = MouseKeysInterceptor.MouseKeyEvent.RELEASE.getKeyCodeValue() + val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, + keyCode, 0, 0, DEVICE_ID, 0) + mouseKeysInterceptor.onKeyEvent(downEvent, 0) + testLooper.dispatchAll() + + // Verify the sendButtonEvent method is called once and capture the arguments + verifyButtonEvents( + arrayOf<Int>( VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE), + arrayOf<Int>( VirtualMouseButtonEvent.BUTTON_PRIMARY) + ) + } + + @Test + fun whenScrollUpKeyIsPressed_scrollEventIsSent() { + // There should be some delay between the downTime of the key event and calling onKeyEvent + val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS + val keyCode = MouseKeysInterceptor.MouseKeyEvent.SCROLL_UP.getKeyCodeValue() + val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, + keyCode, 0, 0, DEVICE_ID, 0) + + mouseKeysInterceptor.onKeyEvent(downEvent, 0) + testLooper.dispatchAll() + + // Verify the sendScrollEvent method is called once and capture the arguments + verifyScrollEvents(arrayOf<Float>(0f), arrayOf<Float>(1.0f)) + } + + private fun verifyRelativeEvents(expectedX: Array<Float>, expectedY: Array<Float>) { + assertEquals(expectedX.size, expectedY.size) + val captor = ArgumentCaptor.forClass(VirtualMouseRelativeEvent::class.java) + Mockito.verify(mockVirtualMouse, Mockito.times(expectedX.size)) + .sendRelativeEvent(captor.capture()) + + for (i in expectedX.indices) { + val captorEvent = captor.allValues[i] + assertEquals(expectedX[i], captorEvent.relativeX) + assertEquals(expectedY[i], captorEvent.relativeY) + } + } + + private fun verifyButtonEvents(actions: Array<Int>, buttons: Array<Int>) { + assertEquals(actions.size, buttons.size) + val captor = ArgumentCaptor.forClass(VirtualMouseButtonEvent::class.java) + Mockito.verify(mockVirtualMouse, Mockito.times(actions.size)) + .sendButtonEvent(captor.capture()) + + for (i in actions.indices) { + val captorEvent = captor.allValues[i] + assertEquals(actions[i], captorEvent.action) + assertEquals(buttons[i], captorEvent.buttonCode) + } + } + + private fun verifyScrollEvents(xAxisMovements: Array<Float>, yAxisMovements: Array<Float>) { + assertEquals(xAxisMovements.size, yAxisMovements.size) + val captor = ArgumentCaptor.forClass(VirtualMouseScrollEvent::class.java) + Mockito.verify(mockVirtualMouse, Mockito.times(xAxisMovements.size)) + .sendScrollEvent(captor.capture()) + + for (i in xAxisMovements.indices) { + val captorEvent = captor.allValues[i] + assertEquals(xAxisMovements[i], captorEvent.xAxisMovement) + assertEquals(yAxisMovements[i], captorEvent.yAxisMovement) + } + } + + private class TrackingInterceptor : BaseEventStreamTransformation() { + val events: Queue<KeyEvent> = LinkedList() + + override fun onKeyEvent(event: KeyEvent, policyFlags: Int) { + events.add(event) + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java index f6da41166403..ffc78110d496 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java @@ -28,7 +28,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.testng.Assert.assertThrows; import android.hardware.biometrics.BiometricConstants; import android.os.Handler; @@ -88,16 +87,14 @@ public class BiometricSchedulerOperationTest { private Handler mHandler; private BiometricSchedulerOperation mInterruptableOperation; private BiometricSchedulerOperation mNonInterruptableOperation; - private boolean mIsDebuggable; @Before public void setUp() { mHandler = new Handler(TestableLooper.get(this).getLooper()); - mIsDebuggable = false; mInterruptableOperation = new BiometricSchedulerOperation(mInterruptableClientMonitor, - mClientCallback, () -> mIsDebuggable); + mClientCallback); mNonInterruptableOperation = new BiometricSchedulerOperation(mNonInterruptableClientMonitor, - mClientCallback, () -> mIsDebuggable); + mClientCallback); when(mInterruptableClientMonitor.isInterruptable()).thenReturn(true); when(mNonInterruptableClientMonitor.isInterruptable()).thenReturn(false); @@ -143,32 +140,13 @@ public class BiometricSchedulerOperationTest { } @Test - public void testSecondStartWithCookieCrashesWhenDebuggable() { + public void testSecondStartWithCookieFails() { final int cookie = 5; - mIsDebuggable = true; when(mInterruptableClientMonitor.getCookie()).thenReturn(cookie); when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - final boolean started = mInterruptableOperation.startWithCookie(mOnStartCallback, cookie); - assertThat(started).isTrue(); - - assertThrows(IllegalStateException.class, - () -> mInterruptableOperation.startWithCookie(mOnStartCallback, cookie)); - } - - @Test - public void testSecondStartWithCookieFailsNicelyWhenNotDebuggable() { - final int cookie = 5; - mIsDebuggable = false; - when(mInterruptableClientMonitor.getCookie()).thenReturn(cookie); - when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - - final boolean started = mInterruptableOperation.startWithCookie(mOnStartCallback, cookie); - assertThat(started).isTrue(); - - final boolean startedAgain = mInterruptableOperation.startWithCookie(mOnStartCallback, - cookie); - assertThat(startedAgain).isFalse(); + assertThat(mInterruptableOperation.startWithCookie(mOnStartCallback, cookie)).isTrue(); + assertThat(mInterruptableOperation.startWithCookie(mOnStartCallback, cookie)).isFalse(); } @Test @@ -217,56 +195,23 @@ public class BiometricSchedulerOperationTest { } @Test - public void secondStartCrashesWhenDebuggable() { - mIsDebuggable = true; + public void secondStartFails() { when(mInterruptableClientMonitor.getCookie()).thenReturn(0); when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - final boolean started = mInterruptableOperation.start(mOnStartCallback); - assertThat(started).isTrue(); - - assertThrows(IllegalStateException.class, () -> mInterruptableOperation.start( - mOnStartCallback)); - } - - @Test - public void secondStartFailsNicelyWhenNotDebuggable() { - mIsDebuggable = false; - when(mInterruptableClientMonitor.getCookie()).thenReturn(0); - when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - - final boolean started = mInterruptableOperation.start(mOnStartCallback); - assertThat(started).isTrue(); - - final boolean startedAgain = mInterruptableOperation.start(mOnStartCallback); - assertThat(startedAgain).isFalse(); + assertThat(mInterruptableOperation.start(mOnStartCallback)).isTrue(); + assertThat(mInterruptableOperation.start(mOnStartCallback)).isFalse(); } @Test public void doesNotStartWithCookie() { - // This class only throws exceptions when debuggable. - mIsDebuggable = true; when(mInterruptableClientMonitor.getCookie()).thenReturn(9); - assertThrows(IllegalStateException.class, - () -> mInterruptableOperation.start(mock(ClientMonitorCallback.class))); - } - @Test - public void cannotRestart() { - // This class only throws exceptions when debuggable. - mIsDebuggable = true; - when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - - mInterruptableOperation.start(mOnStartCallback); - - assertThrows(IllegalStateException.class, - () -> mInterruptableOperation.start(mock(ClientMonitorCallback.class))); + assertThat(mInterruptableOperation.start(mock(ClientMonitorCallback.class))).isFalse(); } @Test - public void abortsNotRunning() { - // This class only throws exceptions when debuggable. - mIsDebuggable = true; + public void abortSuccessfulIfOperationNotRunning() { when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); mInterruptableOperation.abort(); @@ -274,28 +219,17 @@ public class BiometricSchedulerOperationTest { assertThat(mInterruptableOperation.isFinished()).isTrue(); verify(mInterruptableClientMonitor).unableToStart(); verify(mInterruptableClientMonitor).destroy(); - assertThrows(IllegalStateException.class, - () -> mInterruptableOperation.start(mock(ClientMonitorCallback.class))); + assertThat(mInterruptableOperation.start(mock(ClientMonitorCallback.class))).isFalse(); } @Test - public void abortCrashesWhenDebuggableIfOperationIsRunning() { - mIsDebuggable = true; + public void abortFailsIfOperationIsRunning() { when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); mInterruptableOperation.start(mOnStartCallback); - - assertThrows(IllegalStateException.class, () -> mInterruptableOperation.abort()); - } - - @Test - public void abortFailsNicelyWhenNotDebuggableIfOperationIsRunning() { - mIsDebuggable = false; - when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - - mInterruptableOperation.start(mOnStartCallback); - mInterruptableOperation.abort(); + + assertThat(mInterruptableOperation.isFinished()).isFalse(); } @Test @@ -344,21 +278,7 @@ public class BiometricSchedulerOperationTest { } @Test - public void cancelCrashesWhenDebuggableIfOperationIsFinished() { - mIsDebuggable = true; - when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - - mInterruptableOperation.abort(); - assertThat(mInterruptableOperation.isFinished()).isTrue(); - - final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class); - assertThrows(IllegalStateException.class, () -> mInterruptableOperation.cancel(mHandler, - cancelCb)); - } - - @Test - public void cancelFailsNicelyWhenNotDebuggableIfOperationIsFinished() { - mIsDebuggable = false; + public void cancelFailsIfOperationIsFinished() { when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); mInterruptableOperation.abort(); @@ -366,6 +286,9 @@ public class BiometricSchedulerOperationTest { final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class); mInterruptableOperation.cancel(mHandler, cancelCb); + + verify(mInterruptableClientMonitor, never()).cancel(); + verify(mInterruptableClientMonitor, never()).cancelWithoutStarting(any()); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 5d306e152ad7..c1f5a01a8c47 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -89,7 +89,6 @@ import static android.os.UserManager.USER_TYPE_FULL_SECONDARY; import static android.os.UserManager.USER_TYPE_PROFILE_CLONE; import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED; import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE; -import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; import static android.service.notification.Adjustment.KEY_CONTEXTUAL_ACTIONS; import static android.service.notification.Adjustment.KEY_IMPORTANCE; @@ -838,13 +837,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Pretend the shortcut exists List<ShortcutInfo> shortcutInfos = new ArrayList<>(); - ShortcutInfo info = mock(ShortcutInfo.class); - when(info.getPackage()).thenReturn(mPkg); - when(info.getId()).thenReturn(VALID_CONVO_SHORTCUT_ID); - when(info.getUserId()).thenReturn(USER_SYSTEM); - when(info.isLongLived()).thenReturn(true); - when(info.isEnabled()).thenReturn(true); - shortcutInfos.add(info); + shortcutInfos.add(createMockConvoShortcut()); when(mLauncherApps.getShortcuts(any(), any())).thenReturn(shortcutInfos); when(mShortcutServiceInternal.isSharingShortcut(anyInt(), anyString(), anyString(), anyString(), anyInt(), any())).thenReturn(true); @@ -11109,8 +11102,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { BUBBLE_PREFERENCE_ALL /* app */, true /* channel */); - ArgumentCaptor<LauncherApps.Callback> launcherAppsCallback = - ArgumentCaptor.forClass(LauncherApps.Callback.class); + ArgumentCaptor<LauncherApps.ShortcutChangeCallback> shortcutChangeCallback = + ArgumentCaptor.forClass(LauncherApps.ShortcutChangeCallback.class); // Messaging notification with shortcut info Notification.BubbleMetadata metadata = @@ -11131,7 +11124,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Verify: // Make sure we register the callback for shortcut changes - verify(mLauncherApps, times(1)).registerCallback(launcherAppsCallback.capture(), any()); + verify(mShortcutServiceInternal, times(1)).addShortcutChangeCallback( + shortcutChangeCallback.capture()); // yes allowed, yes messaging w/shortcut, yes bubble Notification notif = mService.getNotificationRecord(nr.getSbn().getKey()).getNotification(); @@ -11144,14 +11138,17 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Test: Remove the shortcut when(mLauncherApps.getShortcuts(any(), any())).thenReturn(null); - launcherAppsCallback.getValue().onShortcutsChanged(mPkg, emptyList(), + ArrayList<ShortcutInfo> removedShortcuts = new ArrayList<>(); + removedShortcuts.add(createMockConvoShortcut()); + shortcutChangeCallback.getValue().onShortcutsRemoved(mPkg, removedShortcuts, UserHandle.getUserHandleForUid(mUid)); waitForIdle(); // Verify: // Make sure callback is unregistered - verify(mLauncherApps, times(1)).unregisterCallback(launcherAppsCallback.getValue()); + verify(mShortcutServiceInternal, times(1)).removeShortcutChangeCallback( + shortcutChangeCallback.getValue()); // We're no longer a bubble NotificationRecord notif2 = mService.getNotificationRecord( @@ -11169,8 +11166,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { BUBBLE_PREFERENCE_ALL /* app */, true /* channel */); - ArgumentCaptor<LauncherApps.Callback> launcherAppsCallback = - ArgumentCaptor.forClass(LauncherApps.Callback.class); + ArgumentCaptor<LauncherApps.ShortcutChangeCallback> shortcutChangeCallback = + ArgumentCaptor.forClass(LauncherApps.ShortcutChangeCallback.class); // Messaging notification with shortcut info Notification.BubbleMetadata metadata = new Notification.BubbleMetadata.Builder( @@ -11204,7 +11201,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Verify: // Make sure we register the callback for shortcut changes - verify(mLauncherApps, times(1)).registerCallback(launcherAppsCallback.capture(), any()); + verify(mShortcutServiceInternal, times(1)).addShortcutChangeCallback( + shortcutChangeCallback.capture()); // yes allowed, yes messaging w/shortcut, yes bubble Notification notif = mService.getNotificationRecord(nr.getSbn().getKey()).getNotification(); @@ -11223,7 +11221,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Verify: // Make sure callback is unregistered - verify(mLauncherApps, times(1)).unregisterCallback(launcherAppsCallback.getValue()); + verify(mShortcutServiceInternal, times(1)).removeShortcutChangeCallback( + shortcutChangeCallback.getValue()); } @Test @@ -16263,4 +16262,14 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertThat(r.getChannel().getId()).isEqualTo(NEWS_ID); } + + private ShortcutInfo createMockConvoShortcut() { + ShortcutInfo info = mock(ShortcutInfo.class); + when(info.getPackage()).thenReturn(mPkg); + when(info.getId()).thenReturn(VALID_CONVO_SHORTCUT_ID); + when(info.getUserId()).thenReturn(USER_SYSTEM); + when(info.isLongLived()).thenReturn(true); + when(info.isEnabled()).thenReturn(true); + return info; + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ShortcutHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ShortcutHelperTest.java index a4fb16dc1adc..f008cb671e78 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ShortcutHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ShortcutHelperTest.java @@ -22,17 +22,22 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Notification; +import android.app.NotificationChannel; +import android.app.PendingIntent; import android.app.Person; import android.content.pm.LauncherApps; import android.content.pm.LauncherApps.ShortcutQuery; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutQueryWrapper; import android.content.pm.ShortcutServiceInternal; +import android.graphics.drawable.Icon; import android.os.UserHandle; import android.os.UserManager; import android.service.notification.StatusBarNotification; @@ -50,11 +55,9 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.ArrayList; -import java.util.Collections; import java.util.List; @SmallTest @@ -64,7 +67,6 @@ public class ShortcutHelperTest extends UiServiceTestCase { private static final String SHORTCUT_ID = "shortcut"; private static final String PKG = "pkg"; - private static final String KEY = "key"; private static final Person PERSON = mock(Person.class); @Mock @@ -75,19 +77,11 @@ public class ShortcutHelperTest extends UiServiceTestCase { UserManager mUserManager; @Mock ShortcutServiceInternal mShortcutServiceInternal; - @Mock - NotificationRecord mNr; - @Mock - Notification mNotif; - @Mock - StatusBarNotification mSbn; - @Mock - Notification.BubbleMetadata mBubbleMetadata; - @Mock - ShortcutInfo mShortcutInfo; @Captor private ArgumentCaptor<ShortcutQuery> mShortcutQueryCaptor; + NotificationRecord mNr; + ShortcutHelper mShortcutHelper; @Before @@ -96,137 +90,186 @@ public class ShortcutHelperTest extends UiServiceTestCase { mShortcutHelper = new ShortcutHelper( mLauncherApps, mShortcutListener, mShortcutServiceInternal, mUserManager); - when(mSbn.getPackageName()).thenReturn(PKG); - when(mShortcutInfo.getId()).thenReturn(SHORTCUT_ID); - when(mNotif.getBubbleMetadata()).thenReturn(mBubbleMetadata); - when(mBubbleMetadata.getShortcutId()).thenReturn(SHORTCUT_ID); when(mUserManager.isUserUnlocked(any(UserHandle.class))).thenReturn(true); - setUpMockNotificationRecord(mNr, KEY); + mNr = setUpNotificationRecord(SHORTCUT_ID, PKG, UserHandle.of(UserHandle.USER_SYSTEM)); } - private void setUpMockNotificationRecord(NotificationRecord mockRecord, String key) { - when(mockRecord.getKey()).thenReturn(key); - when(mockRecord.getSbn()).thenReturn(mSbn); - when(mockRecord.getNotification()).thenReturn(mNotif); - when(mockRecord.getShortcutInfo()).thenReturn(mShortcutInfo); + private NotificationRecord setUpNotificationRecord(String shortcutId, + String pkg, + UserHandle user) { + ShortcutInfo shortcutInfo = mock(ShortcutInfo.class); + when(shortcutInfo.getId()).thenReturn(shortcutId); + when(shortcutInfo.getUserHandle()).thenReturn(user); + when(shortcutInfo.isLongLived()).thenReturn(true); + + Notification notification = new Notification.Builder(getContext()) + .setContentTitle("title") + .setShortcutId(shortcutId) + .setBubbleMetadata(new Notification.BubbleMetadata.Builder(shortcutId).build()) + .build(); + + StatusBarNotification sbn = new StatusBarNotification(pkg, pkg, 0, null, + 1000, 2000, notification, user, null, System.currentTimeMillis()); + NotificationRecord record = new NotificationRecord(mContext, sbn, + mock(NotificationChannel.class)); + record.setShortcutInfo(shortcutInfo); + return record; } - private LauncherApps.Callback addShortcutBubbleAndVerifyListener() { - mShortcutHelper.maybeListenForShortcutChangesForBubbles(mNr, - false /* removed */, - null /* handler */); + private LauncherApps.ShortcutChangeCallback addShortcutBubbleAndVerifyListener( + NotificationRecord record) { + mShortcutHelper.maybeListenForShortcutChangesForBubbles(record, false /* removed */); - ArgumentCaptor<LauncherApps.Callback> launcherAppsCallback = - ArgumentCaptor.forClass(LauncherApps.Callback.class); + ArgumentCaptor<LauncherApps.ShortcutChangeCallback> launcherAppsCallback = + ArgumentCaptor.forClass(LauncherApps.ShortcutChangeCallback.class); - verify(mLauncherApps, times(1)).registerCallback( - launcherAppsCallback.capture(), any()); + verify(mShortcutServiceInternal, times(1)).addShortcutChangeCallback( + launcherAppsCallback.capture()); return launcherAppsCallback.getValue(); } @Test public void testBubbleAdded_listenedAdded() { - addShortcutBubbleAndVerifyListener(); + addShortcutBubbleAndVerifyListener(mNr); } @Test + public void testListenerNotifiedOnShortcutRemoved() { + LauncherApps.ShortcutChangeCallback callback = addShortcutBubbleAndVerifyListener(mNr); + + List<ShortcutInfo> removedShortcuts = new ArrayList<>(); + removedShortcuts.add(mNr.getShortcutInfo()); + + callback.onShortcutsRemoved(PKG, removedShortcuts, mNr.getUser()); + verify(mShortcutListener).onShortcutRemoved(mNr.getKey()); + } + + @Test + public void testListenerNotNotified_notMatchingPackage() { + LauncherApps.ShortcutChangeCallback callback = addShortcutBubbleAndVerifyListener(mNr); + + List<ShortcutInfo> removedShortcuts = new ArrayList<>(); + removedShortcuts.add(mNr.getShortcutInfo()); + + callback.onShortcutsRemoved("differentPackage", removedShortcuts, mNr.getUser()); + verify(mShortcutListener, never()).onShortcutRemoved(anyString()); + } + + @Test + public void testListenerNotNotified_notMatchingUser() { + LauncherApps.ShortcutChangeCallback callback = addShortcutBubbleAndVerifyListener(mNr); + + List<ShortcutInfo> removedShortcuts = new ArrayList<>(); + removedShortcuts.add(mNr.getShortcutInfo()); + + callback.onShortcutsRemoved(PKG, removedShortcuts, UserHandle.of(10)); + verify(mShortcutListener, never()).onShortcutRemoved(anyString()); + } + + @Test + public void testListenerNotifiedDifferentUser() { + LauncherApps.ShortcutChangeCallback callback = addShortcutBubbleAndVerifyListener(mNr); + NotificationRecord diffUserRecord = setUpNotificationRecord(SHORTCUT_ID, PKG, + UserHandle.of(10)); + mShortcutHelper.maybeListenForShortcutChangesForBubbles(diffUserRecord, + false /* removed */); + + List<ShortcutInfo> removedShortcuts = new ArrayList<>(); + removedShortcuts.add(mNr.getShortcutInfo()); + + callback.onShortcutsRemoved(PKG, removedShortcuts, mNr.getUser()); + verify(mShortcutListener).onShortcutRemoved(mNr.getKey()); + + reset(mShortcutListener); + removedShortcuts.clear(); + removedShortcuts.add(diffUserRecord.getShortcutInfo()); + + callback.onShortcutsRemoved(PKG, removedShortcuts, diffUserRecord.getUser()); + verify(mShortcutListener).onShortcutRemoved(diffUserRecord.getKey()); + } + + + @Test public void testBubbleRemoved_listenerRemoved() { // First set it up to listen - addShortcutBubbleAndVerifyListener(); + addShortcutBubbleAndVerifyListener(mNr); // Then remove the notif mShortcutHelper.maybeListenForShortcutChangesForBubbles(mNr, - true /* removed */, - null /* handler */); + true /* removed */); - verify(mLauncherApps, times(1)).unregisterCallback(any()); + verify(mShortcutServiceInternal, times(1)).removeShortcutChangeCallback(any()); } @Test public void testBubbleNoLongerHasBubbleMetadata_listenerRemoved() { // First set it up to listen - addShortcutBubbleAndVerifyListener(); + addShortcutBubbleAndVerifyListener(mNr); // Then make it not a bubble - when(mNotif.getBubbleMetadata()).thenReturn(null); + mNr.getNotification().setBubbleMetadata(null); mShortcutHelper.maybeListenForShortcutChangesForBubbles(mNr, - false /* removed */, - null /* handler */); + false /* removed */); - verify(mLauncherApps, times(1)).unregisterCallback(any()); + verify(mShortcutServiceInternal, times(1)).removeShortcutChangeCallback(any()); } @Test public void testBubbleNoLongerHasShortcutId_listenerRemoved() { // First set it up to listen - addShortcutBubbleAndVerifyListener(); + addShortcutBubbleAndVerifyListener(mNr); // Clear out shortcutId - when(mBubbleMetadata.getShortcutId()).thenReturn(null); + mNr.getNotification().setBubbleMetadata(new Notification.BubbleMetadata.Builder( + mock(PendingIntent.class), mock(Icon.class)).build()); mShortcutHelper.maybeListenForShortcutChangesForBubbles(mNr, - false /* removed */, - null /* handler */); + false /* removed */); - verify(mLauncherApps, times(1)).unregisterCallback(any()); + verify(mShortcutServiceInternal, times(1)).removeShortcutChangeCallback(any()); } @Test public void testNotifNoLongerHasShortcut_listenerRemoved() { // First set it up to listen - addShortcutBubbleAndVerifyListener(); - - NotificationRecord validMock1 = Mockito.mock(NotificationRecord.class); - setUpMockNotificationRecord(validMock1, "KEY1"); - - NotificationRecord validMock2 = Mockito.mock(NotificationRecord.class); - setUpMockNotificationRecord(validMock2, "KEY2"); + addShortcutBubbleAndVerifyListener(mNr); - NotificationRecord validMock3 = Mockito.mock(NotificationRecord.class); - setUpMockNotificationRecord(validMock3, "KEY3"); + NotificationRecord record1 = setUpNotificationRecord(SHORTCUT_ID, PKG, + UserHandle.of(UserHandle.USER_SYSTEM)); + NotificationRecord record2 = setUpNotificationRecord(SHORTCUT_ID, PKG, + UserHandle.of(UserHandle.USER_SYSTEM)); + NotificationRecord record3 = setUpNotificationRecord(SHORTCUT_ID, PKG, + UserHandle.of(UserHandle.USER_SYSTEM)); - mShortcutHelper.maybeListenForShortcutChangesForBubbles(validMock1, - false /* removed */, - null /* handler */); + mShortcutHelper.maybeListenForShortcutChangesForBubbles(record1, + false /* removed */); - mShortcutHelper.maybeListenForShortcutChangesForBubbles(validMock2, - false /* removed */, - null /* handler */); + mShortcutHelper.maybeListenForShortcutChangesForBubbles(record2, + false /* removed */); - mShortcutHelper.maybeListenForShortcutChangesForBubbles(validMock3, - false /* removed */, - null /* handler */); + mShortcutHelper.maybeListenForShortcutChangesForBubbles(record3, + false /* removed */); - // Clear out shortcutId of the bubble in the middle, to double check that we don't hit a + // Clear out shortcutId of the bubble in the middle, to double-check that we don't hit a // concurrent modification exception (removing the last bubble would sidestep that check). - when(validMock2.getShortcutInfo()).thenReturn(null); - mShortcutHelper.maybeListenForShortcutChangesForBubbles(validMock2, - false /* removed */, - null /* handler */); + record2.setShortcutInfo(null); + mShortcutHelper.maybeListenForShortcutChangesForBubbles(record2, + false /* removed */); - verify(mLauncherApps, times(1)).unregisterCallback(any()); + verify(mShortcutServiceInternal, times(1)).removeShortcutChangeCallback(any()); } @Test public void testOnShortcutsChanged_listenerRemoved() { // First set it up to listen - LauncherApps.Callback callback = addShortcutBubbleAndVerifyListener(); + LauncherApps.ShortcutChangeCallback callback = addShortcutBubbleAndVerifyListener(mNr); // App shortcuts are removed: - callback.onShortcutsChanged(PKG, Collections.emptyList(), mock(UserHandle.class)); + List<ShortcutInfo> removedShortcuts = new ArrayList<>(); + removedShortcuts.add(mNr.getShortcutInfo()); + callback.onShortcutsRemoved(PKG, removedShortcuts, mNr.getUser()); - verify(mLauncherApps, times(1)).unregisterCallback(any()); - } - - @Test - public void testListenerNotifiedOnShortcutRemoved() { - LauncherApps.Callback callback = addShortcutBubbleAndVerifyListener(); - - List<ShortcutInfo> shortcutInfos = new ArrayList<>(); - when(mLauncherApps.getShortcuts(any(), any())).thenReturn(shortcutInfos); - - callback.onShortcutsChanged(PKG, shortcutInfos, mock(UserHandle.class)); - verify(mShortcutListener).onShortcutRemoved(mNr.getKey()); + verify(mShortcutServiceInternal, times(1)).removeShortcutChangeCallback(any()); } @Test @@ -321,7 +364,6 @@ public class ShortcutHelperTest extends UiServiceTestCase { .isSameInstanceAs(si); } - @Test public void testGetValidShortcutInfo_isValidButUserLocked() { ShortcutInfo si = mock(ShortcutInfo.class); |