diff options
91 files changed, 3082 insertions, 1291 deletions
diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java index b09463e3074b..3bf3067f8410 100644 --- a/core/java/android/app/TaskInfo.java +++ b/core/java/android/app/TaskInfo.java @@ -449,7 +449,8 @@ public class TaskInfo { && isVisible == that.isVisible && isSleeping == that.isSleeping && Objects.equals(mTopActivityLocusId, that.mTopActivityLocusId) - && parentTaskId == that.parentTaskId; + && parentTaskId == that.parentTaskId + && Objects.equals(topActivity, that.topActivity); } /** diff --git a/libs/WindowManager/Shell/res/drawable/caption_desktop_button.xml b/libs/WindowManager/Shell/res/drawable/caption_desktop_button.xml new file mode 100644 index 000000000000..8779cc09715b --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/caption_desktop_button.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32.0dp" + android:height="32.0dp" + android:viewportWidth="32.0" + android:viewportHeight="32.0" +> + <group android:scaleX="0.5" + android:scaleY="0.5" + android:translateX="6.0" + android:translateY="6.0"> + <path + android:fillColor="@android:color/black" + android:pathData="M5.958,37.708Q4.458,37.708 3.354,36.604Q2.25,35.5 2.25,34V18.292Q2.25,16.792 3.354,15.688Q4.458,14.583 5.958,14.583H9.5V5.958Q9.5,4.458 10.625,3.354Q11.75,2.25 13.208,2.25H34Q35.542,2.25 36.646,3.354Q37.75,4.458 37.75,5.958V21.667Q37.75,23.167 36.646,24.271Q35.542,25.375 34,25.375H30.5V34Q30.5,35.5 29.396,36.604Q28.292,37.708 26.792,37.708ZM5.958,34H26.792Q26.792,34 26.792,34Q26.792,34 26.792,34V21.542H5.958V34Q5.958,34 5.958,34Q5.958,34 5.958,34ZM30.5,21.667H34Q34,21.667 34,21.667Q34,21.667 34,21.667V9.208H13.208V14.583H26.833Q28.375,14.583 29.438,15.667Q30.5,16.75 30.5,18.25Z"/> + </group> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/caption_floating_button.xml b/libs/WindowManager/Shell/res/drawable/caption_floating_button.xml new file mode 100644 index 000000000000..ea0fbb0e5d33 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/caption_floating_button.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32.0dp" + android:height="32.0dp" + android:viewportWidth="32.0" + android:viewportHeight="32.0" +> + <group android:scaleX="0.5" + android:scaleY="0.5" + android:translateX="6.0" + android:translateY="6.0"> + <path + android:fillColor="@android:color/black" + android:pathData="M18.167,21.875H29.833V10.208H18.167ZM7.875,35.833Q6.375,35.833 5.271,34.729Q4.167,33.625 4.167,32.125V7.875Q4.167,6.375 5.271,5.271Q6.375,4.167 7.875,4.167H32.125Q33.625,4.167 34.729,5.271Q35.833,6.375 35.833,7.875V32.125Q35.833,33.625 34.729,34.729Q33.625,35.833 32.125,35.833ZM7.875,32.125H32.125Q32.125,32.125 32.125,32.125Q32.125,32.125 32.125,32.125V7.875Q32.125,7.875 32.125,7.875Q32.125,7.875 32.125,7.875H7.875Q7.875,7.875 7.875,7.875Q7.875,7.875 7.875,7.875V32.125Q7.875,32.125 7.875,32.125Q7.875,32.125 7.875,32.125ZM7.875,7.875Q7.875,7.875 7.875,7.875Q7.875,7.875 7.875,7.875V32.125Q7.875,32.125 7.875,32.125Q7.875,32.125 7.875,32.125Q7.875,32.125 7.875,32.125Q7.875,32.125 7.875,32.125V7.875Q7.875,7.875 7.875,7.875Q7.875,7.875 7.875,7.875Z"/> + </group> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/caption_fullscreen_button.xml b/libs/WindowManager/Shell/res/drawable/caption_fullscreen_button.xml new file mode 100644 index 000000000000..c55cbe2d054c --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/caption_fullscreen_button.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32.0dp" + android:height="32.0dp" + android:viewportWidth="32.0" + android:viewportHeight="32.0" +> + <group android:scaleX="0.5" + android:scaleY="0.5" + android:translateX="6.0" + android:translateY="6.0"> + <path + android:fillColor="@android:color/black" + android:pathData="M34.042,14.625V9.333Q34.042,9.333 34.042,9.333Q34.042,9.333 34.042,9.333H28.708V5.708H33.917Q35.458,5.708 36.562,6.833Q37.667,7.958 37.667,9.458V14.625ZM2.375,14.625V9.458Q2.375,7.958 3.479,6.833Q4.583,5.708 6.125,5.708H11.292V9.333H6Q6,9.333 6,9.333Q6,9.333 6,9.333V14.625ZM28.708,34.25V30.667H34.042Q34.042,30.667 34.042,30.667Q34.042,30.667 34.042,30.667V25.333H37.667V30.542Q37.667,32 36.562,33.125Q35.458,34.25 33.917,34.25ZM6.125,34.25Q4.583,34.25 3.479,33.125Q2.375,32 2.375,30.542V25.333H6V30.667Q6,30.667 6,30.667Q6,30.667 6,30.667H11.292V34.25ZM9.333,27.292V12.667H30.708V27.292ZM12.917,23.708H27.125V16.25H12.917ZM12.917,23.708V16.25V23.708Z"/> + </group> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/caption_more_button.xml b/libs/WindowManager/Shell/res/drawable/caption_more_button.xml new file mode 100644 index 000000000000..447df43dfddd --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/caption_more_button.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32.0dp" + android:height="32.0dp" + android:viewportWidth="32.0" + android:viewportHeight="32.0" +> + <group android:scaleX="0.5" + android:scaleY="0.5" + android:translateX="6.0" + android:translateY="6.0"> + <path + android:fillColor="@android:color/black" + android:pathData="M8.083,22.833Q6.917,22.833 6.104,22Q5.292,21.167 5.292,20Q5.292,18.833 6.125,18Q6.958,17.167 8.125,17.167Q9.292,17.167 10.125,18Q10.958,18.833 10.958,20Q10.958,21.167 10.125,22Q9.292,22.833 8.083,22.833ZM20,22.833Q18.833,22.833 18,22Q17.167,21.167 17.167,20Q17.167,18.833 18,18Q18.833,17.167 20,17.167Q21.167,17.167 22,18Q22.833,18.833 22.833,20Q22.833,21.167 22,22Q21.167,22.833 20,22.833ZM31.875,22.833Q30.708,22.833 29.875,22Q29.042,21.167 29.042,20Q29.042,18.833 29.875,18Q30.708,17.167 31.917,17.167Q33.083,17.167 33.896,18Q34.708,18.833 34.708,20Q34.708,21.167 33.875,22Q33.042,22.833 31.875,22.833Z"/> + </group> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/caption_split_screen_button.xml b/libs/WindowManager/Shell/res/drawable/caption_split_screen_button.xml new file mode 100644 index 000000000000..c334a543a86a --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/caption_split_screen_button.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32.0dp" + android:height="32.0dp" + android:viewportWidth="32.0" + android:viewportHeight="32.0" +> + <group android:translateX="6.0" + android:translateY="8.0"> + <path + android:fillColor="@android:color/black" + android:pathData="M18 14L13 14L13 2L18 2L18 14ZM20 14L20 2C20 0.9 19.1 -3.93402e-08 18 -8.74228e-08L13 -3.0598e-07C11.9 -3.54062e-07 11 0.9 11 2L11 14C11 15.1 11.9 16 13 16L18 16C19.1 16 20 15.1 20 14ZM7 14L2 14L2 2L7 2L7 14ZM9 14L9 2C9 0.9 8.1 -5.20166e-07 7 -5.68248e-07L2 -7.86805e-07C0.9 -8.34888e-07 -3.93403e-08 0.9 -8.74228e-08 2L-6.11959e-07 14C-6.60042e-07 15.1 0.9 16 2 16L7 16C8.1 16 9 15.1 9 14Z"/> </group> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/handle_menu_background.xml b/libs/WindowManager/Shell/res/drawable/handle_menu_background.xml new file mode 100644 index 000000000000..e307f007e4a4 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/handle_menu_background.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="210.0dp" + android:height="64.0dp" + android:tint="@color/decor_button_light_color" +> + <group android:scaleX="0.5" + android:scaleY="0.5" + android:translateX="8.0" + android:translateY="8.0" > + <path + android:fillColor="@android:color/white" + android:pathData="M18.3334 14L13.3334 14L13.3334 2L18.3334 2L18.3334 14ZM20.3334 14L20.3334 2C20.3334 0.9 19.4334 -3.93402e-08 18.3334 -8.74228e-08L13.3334 -3.0598e-07C12.2334 -3.54062e-07 11.3334 0.9 11.3334 2L11.3334 14C11.3334 15.1 12.2334 16 13.3334 16L18.3334 16C19.4334 16 20.3334 15.1 20.3334 14ZM7.33337 14L2.33337 14L2.33337 2L7.33337 2L7.33337 14ZM9.33337 14L9.33337 2C9.33337 0.899999 8.43337 -5.20166e-07 7.33337 -5.68248e-07L2.33337 -7.86805e-07C1.23337 -8.34888e-07 0.333374 0.899999 0.333374 2L0.333373 14C0.333373 15.1 1.23337 16 2.33337 16L7.33337 16C8.43337 16 9.33337 15.1 9.33337 14Z"/> + </group> +</vector> diff --git a/libs/WindowManager/Shell/res/layout/caption_handle_menu.xml b/libs/WindowManager/Shell/res/layout/caption_handle_menu.xml new file mode 100644 index 000000000000..d9a140b810f8 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/caption_handle_menu.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> + <!-- + ~ 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. + --> +<com.android.wm.shell.windowdecor.WindowDecorLinearLayout +xmlns:android="http://schemas.android.com/apk/res/android" +android:id="@+id/handle_menu" +android:layout_width="wrap_content" +android:layout_height="wrap_content" +android:gravity="center_horizontal" +android:background="@drawable/decor_caption_title"> + <Button + style="@style/CaptionButtonStyle" + android:id="@+id/fullscreen_button" + android:contentDescription="@string/fullscreen_text" + android:background="@drawable/caption_fullscreen_button"/> + <Button + style="@style/CaptionButtonStyle" + android:id="@+id/split_screen_button" + android:contentDescription="@string/split_screen_text" + android:background="@drawable/caption_split_screen_button"/> + <Button + style="@style/CaptionButtonStyle" + android:id="@+id/floating_button" + android:contentDescription="@string/float_button_text" + android:background="@drawable/caption_floating_button"/> + <Button + style="@style/CaptionButtonStyle" + android:id="@+id/desktop_button" + android:contentDescription="@string/desktop_text" + android:background="@drawable/caption_desktop_button"/> + <Button + style="@style/CaptionButtonStyle" + android:id="@+id/more_button" + android:contentDescription="@string/more_button_text" + android:background="@drawable/caption_more_button"/> +</com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml b/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml index 38cd5702f134..51e634c17532 100644 --- a/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml +++ b/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml @@ -19,14 +19,10 @@ android:id="@+id/caption" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="center_horizontal" android:background="@drawable/decor_caption_title"> <Button + style="@style/CaptionButtonStyle" android:id="@+id/back_button" - android:layout_width="32dp" - android:layout_height="32dp" - android:layout_margin="5dp" - android:padding="4dp" android:contentDescription="@string/back_button_text" android:background="@drawable/decor_back_button_dark" /> @@ -39,11 +35,8 @@ android:contentDescription="@string/handle_text" android:background="@drawable/decor_handle_dark"/> <Button + style="@style/CaptionButtonStyle" android:id="@+id/close_window" - android:layout_width="32dp" - android:layout_height="32dp" - android:layout_margin="5dp" - android:padding="4dp" android:contentDescription="@string/close_button_text" android:background="@drawable/decor_close_button_dark"/> </com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index d8a507469722..9fab3a1eb4b9 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -197,4 +197,14 @@ <string name="back_button_text">Back</string> <!-- Accessibility text for the caption handle [CHAR LIMIT=NONE] --> <string name="handle_text">Handle</string> + <!-- Accessibility text for the handle fullscreen button [CHAR LIMIT=NONE] --> + <string name="fullscreen_text">Fullscreen</string> + <!-- Accessibility text for the handle desktop button [CHAR LIMIT=NONE] --> + <string name="desktop_text">Desktop Mode</string> + <!-- Accessibility text for the handle split screen button [CHAR LIMIT=NONE] --> + <string name="split_screen_text">Split Screen</string> + <!-- Accessibility text for the handle more options button [CHAR LIMIT=NONE] --> + <string name="more_button_text">More</string> + <!-- Accessibility text for the handle floating window button [CHAR LIMIT=NONE] --> + <string name="float_button_text">Float</string> </resources> diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index 19f7c3ef4364..a8597210d72e 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -30,6 +30,13 @@ <item name="android:activityCloseExitAnimation">@anim/forced_resizable_exit</item> </style> + <style name="CaptionButtonStyle"> + <item name="android:layout_width">32dp</item> + <item name="android:layout_height">32dp</item> + <item name="android:layout_margin">5dp</item> + <item name="android:padding">4dp</item> + </style> + <style name="DockedDividerBackground"> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">@dimen/split_divider_bar_width</item> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index 5b7d141591ea..295a2e3c4244 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -86,9 +86,9 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private static final int FLING_ENTER_DURATION = 350; private static final int FLING_EXIT_DURATION = 350; - private final int mDividerWindowWidth; - private final int mDividerInsets; - private final int mDividerSize; + private int mDividerWindowWidth; + private int mDividerInsets; + private int mDividerSize; private final Rect mTempRect = new Rect(); private final Rect mRootBounds = new Rect(); @@ -131,6 +131,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mContext = context.createConfigurationContext(configuration); mOrientation = configuration.orientation; mRotation = configuration.windowConfiguration.getRotation(); + mDensity = configuration.densityDpi; mSplitLayoutHandler = splitLayoutHandler; mDisplayImeController = displayImeController; mSplitWindowManager = new SplitWindowManager(windowName, mContext, configuration, @@ -139,24 +140,22 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mImePositionProcessor = new ImePositionProcessor(mContext.getDisplayId()); mSurfaceEffectPolicy = new ResizingEffectPolicy(parallaxType); - final Resources resources = context.getResources(); - mDividerSize = resources.getDimensionPixelSize(R.dimen.split_divider_bar_width); - mDividerInsets = getDividerInsets(resources, context.getDisplay()); - mDividerWindowWidth = mDividerSize + 2 * mDividerInsets; + updateDividerConfig(mContext); mRootBounds.set(configuration.windowConfiguration.getBounds()); mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null); resetDividerPosition(); - mDimNonImeSide = resources.getBoolean(R.bool.config_dimNonImeAttachedSide); + mDimNonImeSide = mContext.getResources().getBoolean(R.bool.config_dimNonImeAttachedSide); updateInvisibleRect(); } - private int getDividerInsets(Resources resources, Display display) { + private void updateDividerConfig(Context context) { + final Resources resources = context.getResources(); + final Display display = context.getDisplay(); final int dividerInset = resources.getDimensionPixelSize( com.android.internal.R.dimen.docked_stack_divider_insets); - int radius = 0; RoundedCorner corner = display.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT); radius = corner != null ? Math.max(radius, corner.getRadius()) : radius; @@ -167,7 +166,9 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange corner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT); radius = corner != null ? Math.max(radius, corner.getRadius()) : radius; - return Math.max(dividerInset, radius); + mDividerInsets = Math.max(dividerInset, radius); + mDividerSize = resources.getDimensionPixelSize(R.dimen.split_divider_bar_width); + mDividerWindowWidth = mDividerSize + 2 * mDividerInsets; } /** Gets bounds of the primary split with screen based coordinate. */ @@ -309,6 +310,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mRotation = rotation; mDensity = density; mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null); + updateDividerConfig(mContext); initDividerPosition(mTempRect); updateInvisibleRect(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java index 16f1d1c2944c..f81c9f80830a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java @@ -23,6 +23,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager.RunningTaskInfo; import android.app.RemoteAction; @@ -70,6 +71,11 @@ public interface PipMenuController { void setAppActions(List<RemoteAction> appActions, RemoteAction closeAction); /** + * Wait until the next frame to run the given Runnable. + */ + void runWithNextFrame(@NonNull Runnable runnable); + + /** * Resize the PiP menu with the given bounds. The PiP SurfaceControl is given if there is a * need to synchronize the movements on the same frame as PiP. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index f170e774739f..2d7c5ce6feb5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -179,8 +179,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // This is necessary in case there was a resize animation ongoing when exit PIP // started, in which case the first resize will be skipped to let the exit // operation handle the final resize out of PIP mode. See b/185306679. - finishResize(tx, destinationBounds, direction, animationType); - sendOnPipTransitionFinished(direction); + finishResizeDelayedIfNeeded(() -> { + finishResize(tx, destinationBounds, direction, animationType); + sendOnPipTransitionFinished(direction); + }); } } @@ -196,6 +198,34 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } }; + /** + * Finishes resizing the PiP, delaying the operation if it has to be synced with the PiP menu. + * + * This is done to avoid a race condition between the last transaction applied in + * onAnimationUpdate and the finishResize in onAnimationEnd. finishResize creates a + * WindowContainerTransaction, which is to be applied by WmCore later. It may happen that it + * gets applied before the transaction created by the last onAnimationUpdate. As a result of + * this, the PiP surface may get scaled after the new bounds are applied by WmCore, which + * makes the PiP surface have unexpected bounds. To avoid this, we delay the finishResize + * operation until the next frame. This aligns the last onAnimationUpdate transaction with the + * WCT application. + * + * The race only happens when the PiP surface transaction has to be synced with the PiP menu + * due to the necessity for a delay when syncing the PiP surface, the PiP menu surface and + * the PiP menu contents. + */ + private void finishResizeDelayedIfNeeded(Runnable finishResizeRunnable) { + if (!shouldSyncPipTransactionWithMenu()) { + finishResizeRunnable.run(); + return; + } + mPipMenuController.runWithNextFrame(finishResizeRunnable); + } + + private boolean shouldSyncPipTransactionWithMenu() { + return mPipMenuController.isMenuVisible(); + } + @VisibleForTesting final PipTransitionController.PipTransitionCallback mPipTransitionCallback = new PipTransitionController.PipTransitionCallback() { @@ -221,7 +251,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, @Override public boolean handlePipTransaction(SurfaceControl leash, SurfaceControl.Transaction tx, Rect destinationBounds) { - if (mPipMenuController.isMenuVisible()) { + if (shouldSyncPipTransactionWithMenu()) { mPipMenuController.movePipMenu(leash, tx, destinationBounds); return true; } @@ -1223,7 +1253,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mSurfaceTransactionHelper .crop(tx, mLeash, toBounds) .round(tx, mLeash, mPipTransitionState.isInPip()); - if (mPipMenuController.isMenuVisible()) { + if (shouldSyncPipTransactionWithMenu()) { mPipMenuController.resizePipMenu(mLeash, tx, toBounds); } else { tx.apply(); @@ -1265,7 +1295,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mSurfaceTransactionHelper .scale(tx, mLeash, startBounds, toBounds, degrees) .round(tx, mLeash, startBounds, toBounds); - if (mPipMenuController.isMenuVisible()) { + if (shouldSyncPipTransactionWithMenu()) { mPipMenuController.movePipMenu(mLeash, tx, toBounds); } else { tx.apply(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java index 281ea530e9e1..27902b2231ba 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java @@ -305,6 +305,18 @@ public class PhonePipMenuController implements PipMenuController { showResizeHandle); } + @Override + public void runWithNextFrame(Runnable runnable) { + if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) { + runnable.run(); + } + + mPipMenuView.getViewRootImpl().registerRtFrameCallback(frame -> { + mMainHandler.post(runnable); + }); + mPipMenuView.invalidate(); + } + /** * Move the PiP menu, which does a translation and possibly a scale transformation. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index 4ce45e142c64..7d4b43be4f73 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -466,6 +466,18 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } @Override + public void runWithNextFrame(Runnable runnable) { + if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) { + runnable.run(); + } + + mPipMenuView.getViewRootImpl().registerRtFrameCallback(frame -> { + mMainHandler.post(runnable); + }); + mPipMenuView.invalidate(); + } + + @Override public void movePipMenu(SurfaceControl pipLeash, SurfaceControl.Transaction transaction, Rect pipDestBounds) { if (DEBUG) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index dca516a327b0..36dd8edaa8b7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -26,11 +26,16 @@ import android.app.ActivityTaskManager; import android.content.Context; import android.hardware.input.InputManager; import android.os.Handler; +import android.os.Looper; import android.os.SystemClock; import android.util.Log; import android.util.SparseArray; import android.view.Choreographer; +import android.view.InputChannel; import android.view.InputDevice; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.InputMonitor; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; @@ -64,8 +69,11 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private final SyncTransactionQueue mSyncQueue; private FreeformTaskTransitionStarter mTransitionStarter; private DesktopModeController mDesktopModeController; + private EventReceiver mEventReceiver; + private InputMonitor mInputMonitor; private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>(); + private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl(); public CaptionWindowDecorViewModel( Context context, @@ -108,12 +116,19 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mSyncQueue); mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); - TaskPositioner taskPositioner = new TaskPositioner(mTaskOrganizer, windowDecoration); + TaskPositioner taskPositioner = new TaskPositioner(mTaskOrganizer, windowDecoration, + mDragStartListener); CaptionTouchEventListener touchEventListener = new CaptionTouchEventListener(taskInfo, taskPositioner); windowDecoration.setCaptionListeners(touchEventListener, touchEventListener); windowDecoration.setDragResizeCallback(taskPositioner); setupWindowDecorationForTransition(taskInfo, startT, finishT); + if (mInputMonitor == null) { + mInputMonitor = InputManager.getInstance().monitorGestureInput( + "caption-touch", mContext.getDisplayId()); + mEventReceiver = new EventReceiver( + mInputMonitor.getInputChannel(), Looper.myLooper()); + } return true; } @@ -165,6 +180,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { @Override public void onClick(View v) { + CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); final int id = v.getId(); if (id == R.id.close_window) { WindowContainerTransaction wct = new WindowContainerTransaction(); @@ -176,6 +192,15 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } } else if (id == R.id.back_button) { injectBackKey(); + } else if (id == R.id.caption_handle) { + decoration.createHandleMenu(); + } else if (id == R.id.desktop_button) { + mDesktopModeController.setDesktopModeActive(true); + decoration.closeHandleMenu(); + } else if (id == R.id.fullscreen_button) { + mDesktopModeController.setDesktopModeActive(false); + decoration.closeHandleMenu(); + decoration.setButtonVisibility(); } } private void injectBackKey() { @@ -257,6 +282,36 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } } + // InputEventReceiver to listen for touch input outside of caption bounds + private class EventReceiver extends InputEventReceiver { + EventReceiver(InputChannel channel, Looper looper) { + super(channel, looper); + } + + @Override + public void onInputEvent(InputEvent event) { + boolean handled = false; + if (event instanceof MotionEvent + && ((MotionEvent) event).getActionMasked() == MotionEvent.ACTION_UP) { + handled = true; + CaptionWindowDecorViewModel.this.handleMotionEvent((MotionEvent) event); + } + finishInputEvent(event, handled); + } + } + + // If any input received is outside of caption bounds, turn off handle menu + private void handleMotionEvent(MotionEvent ev) { + int size = mWindowDecorByTaskId.size(); + for (int i = 0; i < size; i++) { + CaptionWindowDecoration decoration = mWindowDecorByTaskId.valueAt(i); + if (decoration != null) { + decoration.closeHandleMenuIfNeeded(ev); + } + } + } + + private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) return true; return DesktopModeStatus.IS_SUPPORTED @@ -264,4 +319,11 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { && mDisplayController.getDisplayContext(taskInfo.displayId) .getResources().getConfiguration().smallestScreenWidthDp >= 600; } + + private class DragStartListenerImpl implements TaskPositioner.DragStartListener{ + @Override + public void onDragStart(int taskId) { + mWindowDecorByTaskId.get(taskId).closeHandleMenu(); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 9d61c14e1435..03cad043ed67 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -20,10 +20,14 @@ import android.app.ActivityManager; import android.app.WindowConfiguration; import android.content.Context; import android.content.res.ColorStateList; +import android.content.res.Resources; import android.graphics.Color; +import android.graphics.Point; +import android.graphics.Rect; import android.graphics.drawable.VectorDrawable; import android.os.Handler; import android.view.Choreographer; +import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; import android.view.ViewConfiguration; @@ -58,6 +62,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL private boolean mDesktopActive; + private AdditionalWindow mHandleMenu; + CaptionWindowDecoration( Context context, DisplayController displayController, @@ -123,7 +129,20 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL if (isDragResizeable) { mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId); } + final Resources resources = mDecorWindowContext.getResources(); + final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds(); + final int captionHeight = loadDimensionPixelSize(resources, + mRelayoutParams.mCaptionHeightId); + final int captionWidth = loadDimensionPixelSize(resources, + mRelayoutParams.mCaptionWidthId); + final int captionLeft = taskBounds.width() / 2 + - captionWidth / 2; + final int captionTop = taskBounds.top + <= captionHeight / 2 ? 0 : -captionHeight / 2; + mRelayoutParams.setCaptionPosition(captionLeft, captionTop); + relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); + taskInfo = null; // Clear it just in case we use it accidentally mTaskOrganizer.applyTransaction(wct); @@ -137,15 +156,14 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL } // If this task is not focused, do not show caption. - setCaptionVisibility(taskInfo.isFocused); + setCaptionVisibility(mTaskInfo.isFocused); // Only handle should show if Desktop Mode is inactive. boolean desktopCurrentStatus = DesktopModeStatus.isActive(mContext); - if (mDesktopActive != desktopCurrentStatus && taskInfo.isFocused) { + if (mDesktopActive != desktopCurrentStatus && mTaskInfo.isFocused) { mDesktopActive = desktopCurrentStatus; setButtonVisibility(); } - taskInfo = null; // Clear it just in case we use it accidentally if (!isDragResizeable) { closeDragResizeListener(); @@ -184,9 +202,22 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL back.setOnClickListener(mOnCaptionButtonClickListener); View handle = caption.findViewById(R.id.caption_handle); handle.setOnTouchListener(mOnCaptionTouchListener); + handle.setOnClickListener(mOnCaptionButtonClickListener); setButtonVisibility(); } + private void setupHandleMenu() { + View menu = mHandleMenu.mWindowViewHost.getView(); + View fullscreen = menu.findViewById(R.id.fullscreen_button); + fullscreen.setOnClickListener(mOnCaptionButtonClickListener); + View desktop = menu.findViewById(R.id.desktop_button); + desktop.setOnClickListener(mOnCaptionButtonClickListener); + View split = menu.findViewById(R.id.split_screen_button); + split.setOnClickListener(mOnCaptionButtonClickListener); + View more = menu.findViewById(R.id.more_button); + more.setOnClickListener(mOnCaptionButtonClickListener); + } + /** * Sets caption visibility based on task focus. * @@ -194,8 +225,9 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL */ private void setCaptionVisibility(boolean visible) { int v = visible ? View.VISIBLE : View.GONE; - View caption = mResult.mRootView.findViewById(R.id.caption); - caption.setVisibility(v); + View captionView = mResult.mRootView.findViewById(R.id.caption); + captionView.setVisibility(v); + if (!visible) closeHandleMenu(); } /** @@ -203,6 +235,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL * */ public void setButtonVisibility() { + mDesktopActive = DesktopModeStatus.isActive(mContext); int v = mDesktopActive ? View.VISIBLE : View.GONE; View caption = mResult.mRootView.findViewById(R.id.caption); View back = caption.findViewById(R.id.back_button); @@ -220,6 +253,10 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL caption.getBackground().setTint(v == View.VISIBLE ? Color.WHITE : Color.TRANSPARENT); } + public boolean isHandleMenuActive() { + return mHandleMenu != null; + } + private void closeDragResizeListener() { if (mDragResizeListener == null) { return; @@ -228,9 +265,67 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL mDragResizeListener = null; } + /** + * Create and display handle menu window + */ + public void createHandleMenu() { + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + final Resources resources = mDecorWindowContext.getResources(); + int x = mRelayoutParams.mCaptionX; + int y = mRelayoutParams.mCaptionY; + int width = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionWidthId); + int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId); + String namePrefix = "Caption Menu"; + mHandleMenu = addWindow(R.layout.caption_handle_menu, namePrefix, t, + x - mResult.mDecorContainerOffsetX, y - mResult.mDecorContainerOffsetY, + width, height); + mSyncQueue.runInSync(transaction -> { + transaction.merge(t); + t.close(); + }); + setupHandleMenu(); + } + + /** + * Close the handle menu window + */ + public void closeHandleMenu() { + if (!isHandleMenuActive()) return; + mHandleMenu.releaseView(); + mHandleMenu = null; + } + + @Override + void releaseViews() { + closeHandleMenu(); + super.releaseViews(); + } + + /** + * Close an open handle menu if input is outside of menu coordinates + * @param ev the tapped point to compare against + * @return + */ + public void closeHandleMenuIfNeeded(MotionEvent ev) { + if (mHandleMenu != null) { + Point positionInParent = mTaskOrganizer.getRunningTaskInfo(mTaskInfo.taskId) + .positionInParent; + final Resources resources = mDecorWindowContext.getResources(); + ev.offsetLocation(-mRelayoutParams.mCaptionX, -mRelayoutParams.mCaptionY); + ev.offsetLocation(-positionInParent.x, -positionInParent.y); + int width = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionWidthId); + int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId); + if (!(ev.getX() >= 0 && ev.getY() >= 0 + && ev.getX() <= width && ev.getY() <= height)) { + closeHandleMenu(); + } + } + } + @Override public void close() { closeDragResizeListener(); + closeHandleMenu(); super.close(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java index 27c10114ac0e..f0f2db7ded80 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java @@ -42,14 +42,18 @@ class TaskPositioner implements DragResizeCallback { private final Rect mResizeTaskBounds = new Rect(); private int mCtrlType; + private DragStartListener mDragStartListener; - TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration) { + TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration, + DragStartListener dragStartListener) { mTaskOrganizer = taskOrganizer; mWindowDecoration = windowDecoration; + mDragStartListener = dragStartListener; } @Override public void onDragResizeStart(int ctrlType, float x, float y) { + mDragStartListener.onDragStart(mWindowDecoration.mTaskInfo.taskId); mCtrlType = ctrlType; mTaskBoundsAtDragStart.set( @@ -97,4 +101,12 @@ class TaskPositioner implements DragResizeCallback { mTaskOrganizer.applyTransaction(wct); } } + + interface DragStartListener { + /** + * Inform the implementing class that a drag resize has started + * @param taskId id of this positioner's {@link WindowDecoration} + */ + void onDragStart(int taskId); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index b314163802ca..7ecb3f3f6355 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -200,16 +200,17 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> final Rect taskBounds = taskConfig.windowConfiguration.getBounds(); final Resources resources = mDecorWindowContext.getResources(); - final int decorContainerOffsetX = -loadDimensionPixelSize(resources, params.mOutsetLeftId); - final int decorContainerOffsetY = -loadDimensionPixelSize(resources, params.mOutsetTopId); + outResult.mDecorContainerOffsetX = -loadDimensionPixelSize(resources, params.mOutsetLeftId); + outResult.mDecorContainerOffsetY = -loadDimensionPixelSize(resources, params.mOutsetTopId); outResult.mWidth = taskBounds.width() + loadDimensionPixelSize(resources, params.mOutsetRightId) - - decorContainerOffsetX; + - outResult.mDecorContainerOffsetX; outResult.mHeight = taskBounds.height() + loadDimensionPixelSize(resources, params.mOutsetBottomId) - - decorContainerOffsetY; + - outResult.mDecorContainerOffsetY; startT.setPosition( - mDecorationContainerSurface, decorContainerOffsetX, decorContainerOffsetY) + mDecorationContainerSurface, + outResult.mDecorContainerOffsetX, outResult.mDecorContainerOffsetY) .setWindowCrop(mDecorationContainerSurface, outResult.mWidth, outResult.mHeight) // TODO(b/244455401): Change the z-order when it's better organized @@ -252,14 +253,11 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> final int captionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId); final int captionWidth = loadDimensionPixelSize(resources, params.mCaptionWidthId); - //Prevent caption from going offscreen if task is too high up - final int captionYPos = taskBounds.top <= captionHeight / 2 ? 0 : captionHeight / 2; - startT.setPosition( - mCaptionContainerSurface, -decorContainerOffsetX - + taskBounds.width() / 2 - captionWidth / 2, - -decorContainerOffsetY - captionYPos) - .setWindowCrop(mCaptionContainerSurface, taskBounds.width(), captionHeight) + mCaptionContainerSurface, + -outResult.mDecorContainerOffsetX + params.mCaptionX, + -outResult.mDecorContainerOffsetY + params.mCaptionY) + .setWindowCrop(mCaptionContainerSurface, captionWidth, captionHeight) .show(mCaptionContainerSurface); if (mCaptionWindowManager == null) { @@ -292,7 +290,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> // Caption insets mCaptionInsetsRect.set(taskBounds); mCaptionInsetsRect.bottom = - mCaptionInsetsRect.top + captionHeight - captionYPos; + mCaptionInsetsRect.top + captionHeight + params.mCaptionY; wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect, CAPTION_INSETS_TYPES); } else { @@ -302,10 +300,10 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> // Task surface itself Point taskPosition = mTaskInfo.positionInParent; mTaskSurfaceCrop.set( - decorContainerOffsetX, - decorContainerOffsetY, - outResult.mWidth + decorContainerOffsetX, - outResult.mHeight + decorContainerOffsetY); + outResult.mDecorContainerOffsetX, + outResult.mDecorContainerOffsetY, + outResult.mWidth + outResult.mDecorContainerOffsetX, + outResult.mHeight + outResult.mDecorContainerOffsetY); startT.show(mTaskSurface); finishT.setPosition(mTaskSurface, taskPosition.x, taskPosition.y) .setCrop(mTaskSurface, mTaskSurfaceCrop); @@ -326,7 +324,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> return true; } - private void releaseViews() { + void releaseViews() { if (mViewHost != null) { mViewHost.release(); mViewHost = null; @@ -369,20 +367,60 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> releaseViews(); } - private static int loadDimensionPixelSize(Resources resources, int resourceId) { + static int loadDimensionPixelSize(Resources resources, int resourceId) { if (resourceId == Resources.ID_NULL) { return 0; } return resources.getDimensionPixelSize(resourceId); } - private static float loadDimension(Resources resources, int resourceId) { + static float loadDimension(Resources resources, int resourceId) { if (resourceId == Resources.ID_NULL) { return 0; } return resources.getDimension(resourceId); } + /** + * Create a window associated with this WindowDecoration. + * Note that subclass must dispose of this when the task is hidden/closed. + * @param layoutId layout to make the window from + * @param t the transaction to apply + * @param xPos x position of new window + * @param yPos y position of new window + * @param width width of new window + * @param height height of new window + * @return + */ + AdditionalWindow addWindow(int layoutId, String namePrefix, + SurfaceControl.Transaction t, int xPos, int yPos, int width, int height) { + final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get(); + SurfaceControl windowSurfaceControl = builder + .setName(namePrefix + " of Task=" + mTaskInfo.taskId) + .setContainerLayer() + .setParent(mDecorationContainerSurface) + .build(); + View v = LayoutInflater.from(mDecorWindowContext).inflate(layoutId, null); + + t.setPosition( + windowSurfaceControl, xPos, yPos) + .setWindowCrop(windowSurfaceControl, width, height) + .show(windowSurfaceControl); + final WindowManager.LayoutParams lp = + new WindowManager.LayoutParams(width, height, + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); + lp.setTitle("Additional window of Task=" + mTaskInfo.taskId); + lp.setTrustedOverlay(); + WindowlessWindowManager windowManager = new WindowlessWindowManager(mTaskInfo.configuration, + windowSurfaceControl, null /* hostInputToken */); + SurfaceControlViewHost viewHost = mSurfaceControlViewHostFactory + .create(mDecorWindowContext, mDisplay, windowManager); + viewHost.setView(v, lp); + return new AdditionalWindow(windowSurfaceControl, viewHost, + mSurfaceControlTransactionSupplier); + } + static class RelayoutParams{ RunningTaskInfo mRunningTaskInfo; int mLayoutResId; @@ -395,6 +433,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> int mOutsetLeftId; int mOutsetRightId; + int mCaptionX; + int mCaptionY; + void setOutsets(int leftId, int topId, int rightId, int bottomId) { mOutsetLeftId = leftId; mOutsetTopId = topId; @@ -402,6 +443,11 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mOutsetBottomId = bottomId; } + void setCaptionPosition(int left, int top) { + mCaptionX = left; + mCaptionY = top; + } + void reset() { mLayoutResId = Resources.ID_NULL; mCaptionHeightId = Resources.ID_NULL; @@ -412,6 +458,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mOutsetBottomId = Resources.ID_NULL; mOutsetLeftId = Resources.ID_NULL; mOutsetRightId = Resources.ID_NULL; + + mCaptionX = 0; + mCaptionY = 0; } } @@ -419,10 +468,14 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> int mWidth; int mHeight; T mRootView; + int mDecorContainerOffsetX; + int mDecorContainerOffsetY; void reset() { mWidth = 0; mHeight = 0; + mDecorContainerOffsetX = 0; + mDecorContainerOffsetY = 0; mRootView = null; } } @@ -432,4 +485,41 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> return new SurfaceControlViewHost(c, d, wmm); } } + + /** + * Subclass for additional windows associated with this WindowDecoration + */ + static class AdditionalWindow { + SurfaceControl mWindowSurface; + SurfaceControlViewHost mWindowViewHost; + Supplier<SurfaceControl.Transaction> mTransactionSupplier; + + private AdditionalWindow(SurfaceControl surfaceControl, + SurfaceControlViewHost surfaceControlViewHost, + Supplier<SurfaceControl.Transaction> transactionSupplier) { + mWindowSurface = surfaceControl; + mWindowViewHost = surfaceControlViewHost; + mTransactionSupplier = transactionSupplier; + } + + void releaseView() { + WindowlessWindowManager windowManager = mWindowViewHost.getWindowlessWM(); + + if (mWindowViewHost != null) { + mWindowViewHost.release(); + mWindowViewHost = null; + } + windowManager = null; + final SurfaceControl.Transaction t = mTransactionSupplier.get(); + boolean released = false; + if (mWindowSurface != null) { + t.remove(mWindowSurface); + mWindowSurface = null; + released = true; + } + if (released) { + t.apply(); + } + } + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index 4d37e5dbc4dc..15181b1549f5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -35,6 +35,7 @@ import static org.mockito.Mockito.verify; import android.app.ActivityManager; import android.content.Context; +import android.content.res.Resources; import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; @@ -64,6 +65,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; +import org.mockito.Mockito; import java.util.ArrayList; import java.util.List; @@ -102,12 +104,14 @@ public class WindowDecorationTests extends ShellTestCase { private final List<SurfaceControl.Builder> mMockSurfaceControlBuilders = new ArrayList<>(); private SurfaceControl.Transaction mMockSurfaceControlStartT; private SurfaceControl.Transaction mMockSurfaceControlFinishT; + private SurfaceControl.Transaction mMockSurfaceControlAddWindowT; private WindowDecoration.RelayoutParams mRelayoutParams = new WindowDecoration.RelayoutParams(); @Before public void setUp() { mMockSurfaceControlStartT = createMockSurfaceControlTransaction(); mMockSurfaceControlFinishT = createMockSurfaceControlTransaction(); + mMockSurfaceControlAddWindowT = createMockSurfaceControlTransaction(); doReturn(mMockSurfaceControlViewHost).when(mMockSurfaceControlViewHostFactory) .create(any(), any(), any()); @@ -227,8 +231,8 @@ public class WindowDecorationTests extends ShellTestCase { verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface); verify(captionContainerSurfaceBuilder).setContainerLayer(); - verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, -46, 8); - verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 64); + verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, 20, 40); + verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 432, 64); verify(mMockSurfaceControlStartT).show(captionContainerSurface); verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any()); @@ -242,7 +246,7 @@ public class WindowDecorationTests extends ShellTestCase { verify(mMockView).setTaskFocusState(true); verify(mMockWindowContainerTransaction) .addRectInsetsProvider(taskInfo.token, - new Rect(100, 300, 400, 332), + new Rect(100, 300, 400, 364), new int[] { InsetsState.ITYPE_CAPTION_BAR }); } @@ -366,6 +370,71 @@ public class WindowDecorationTests extends ShellTestCase { verify(mMockSurfaceControlViewHost).setView(same(mMockView), any()); } + @Test + public void testAddWindow() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + + final SurfaceControl decorContainerSurface = mock(SurfaceControl.class); + final SurfaceControl.Builder decorContainerSurfaceBuilder = + createMockSurfaceControlBuilder(decorContainerSurface); + mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder); + final SurfaceControl taskBackgroundSurface = mock(SurfaceControl.class); + final SurfaceControl.Builder taskBackgroundSurfaceBuilder = + createMockSurfaceControlBuilder(taskBackgroundSurface); + mMockSurfaceControlBuilders.add(taskBackgroundSurfaceBuilder); + final SurfaceControl captionContainerSurface = mock(SurfaceControl.class); + final SurfaceControl.Builder captionContainerSurfaceBuilder = + createMockSurfaceControlBuilder(captionContainerSurface); + mMockSurfaceControlBuilders.add(captionContainerSurfaceBuilder); + + final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); + mMockSurfaceControlTransactions.add(t); + final ActivityManager.TaskDescription.Builder taskDescriptionBuilder = + new ActivityManager.TaskDescription.Builder() + .setBackgroundColor(Color.YELLOW); + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setTaskDescriptionBuilder(taskDescriptionBuilder) + .setBounds(TASK_BOUNDS) + .setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y) + .setVisible(true) + .build(); + taskInfo.isFocused = true; + taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); + final SurfaceControl taskSurface = mock(SurfaceControl.class); + final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); + windowDecor.relayout(taskInfo); + + final SurfaceControl additionalWindowSurface = mock(SurfaceControl.class); + final SurfaceControl.Builder additionalWindowSurfaceBuilder = + createMockSurfaceControlBuilder(additionalWindowSurface); + mMockSurfaceControlBuilders.add(additionalWindowSurfaceBuilder); + + WindowDecoration.AdditionalWindow additionalWindow = windowDecor.addTestWindow(); + + verify(additionalWindowSurfaceBuilder).setContainerLayer(); + verify(additionalWindowSurfaceBuilder).setParent(decorContainerSurface); + verify(additionalWindowSurfaceBuilder).build(); + verify(mMockSurfaceControlAddWindowT).setPosition(additionalWindowSurface, 20, 40); + verify(mMockSurfaceControlAddWindowT).setWindowCrop(additionalWindowSurface, 432, 64); + verify(mMockSurfaceControlAddWindowT).show(additionalWindowSurface); + verify(mMockSurfaceControlViewHostFactory, Mockito.times(2)) + .create(any(), eq(defaultDisplay), any()); + assertThat(additionalWindow.mWindowViewHost).isNotNull(); + + additionalWindow.releaseView(); + + assertThat(additionalWindow.mWindowViewHost).isNull(); + assertThat(additionalWindow.mWindowSurface).isNull(); + } + private TestWindowDecoration createWindowDecoration( ActivityManager.RunningTaskInfo taskInfo, SurfaceControl testSurface) { return new TestWindowDecoration(InstrumentationRegistry.getInstrumentation().getContext(), @@ -429,5 +498,20 @@ public class WindowDecorationTests extends ShellTestCase { relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT, mMockWindowContainerTransaction, mMockView, mRelayoutResult); } + + private WindowDecoration.AdditionalWindow addTestWindow() { + final Resources resources = mDecorWindowContext.getResources(); + int x = mRelayoutParams.mCaptionX; + int y = mRelayoutParams.mCaptionY; + int width = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionWidthId); + int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId); + String name = "Test Window"; + WindowDecoration.AdditionalWindow additionalWindow = + addWindow(R.layout.caption_handle_menu, name, mMockSurfaceControlAddWindowT, + x - mRelayoutResult.mDecorContainerOffsetX, + y - mRelayoutResult.mDecorContainerOffsetY, + width, height); + return additionalWindow; + } } } diff --git a/packages/SystemUI/res/values/integers.xml b/packages/SystemUI/res/values/integers.xml index e30d4415a0c4..8d4431520c75 100644 --- a/packages/SystemUI/res/values/integers.xml +++ b/packages/SystemUI/res/values/integers.xml @@ -35,4 +35,6 @@ <!-- Percentage of displacement for items in QQS to guarantee matching with bottom of clock at fade_out_complete_frame --> <dimen name="percent_displacement_at_fade_out" format="float">0.1066</dimen> + + <integer name="qs_carrier_max_em">7</integer> </resources>
\ No newline at end of file diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl index e743ec87bd1c..bfbe88c475ac 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl @@ -20,6 +20,7 @@ import android.graphics.Rect; import android.graphics.Region; import android.os.Bundle; import android.view.MotionEvent; +import android.view.SurfaceControl; import com.android.systemui.shared.recents.ISystemUiProxy; oneway interface IOverviewProxy { @@ -44,12 +45,6 @@ oneway interface IOverviewProxy { void onOverviewHidden(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) = 8; /** - * Sent when there was an action on one of the onboarding tips view. - * TODO: Move this implementation to SystemUI completely - */ - void onTip(int actionType, int viewType) = 10; - - /** * Sent when device assistant changes its default assistant whether it is available or not. */ void onAssistantAvailable(boolean available) = 13; @@ -60,13 +55,6 @@ oneway interface IOverviewProxy { void onAssistantVisibilityChanged(float visibility) = 14; /** - * Sent when back is triggered. - * TODO: Move this implementation to SystemUI completely - */ - void onBackAction(boolean completed, int downX, int downY, boolean isButton, - boolean gestureSwipeLeft) = 15; - - /** * Sent when some system ui state changes. */ void onSystemUiStateChanged(int stateFlags) = 16; @@ -115,4 +103,9 @@ oneway interface IOverviewProxy { * Sent when split keyboard shortcut is triggered to enter stage split. */ void enterStageSplitFromRunningApp(boolean leftOrTop) = 25; + + /** + * Sent when the surface for navigation bar is created or changed + */ + void onNavigationBarSurface(in SurfaceControl surface) = 26; } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl index 2b2b05ce2fbf..b99b72bb4275 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl @@ -106,9 +106,6 @@ interface ISystemUiProxy { /** Sets home rotation enabled. */ void setHomeRotationEnabled(boolean enabled) = 45; - /** Notifies that a swipe-up gesture has started */ - oneway void notifySwipeUpGestureStarted() = 46; - /** Notifies when taskbar status updated */ oneway void notifyTaskbarStatus(boolean visible, boolean stashed) = 47; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index 80c6c48cb7ee..37da2c7d3379 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -120,7 +120,7 @@ public class AuthContainerView extends LinearLayout @VisibleForTesting final BiometricCallback mBiometricCallback; @Nullable private AuthBiometricView mBiometricView; - @Nullable private AuthCredentialView mCredentialView; + @VisibleForTesting @Nullable AuthCredentialView mCredentialView; private final AuthPanelController mPanelController; private final FrameLayout mFrameLayout; private final ImageView mBackgroundView; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java index f9e44a0c1724..85cb39849f1f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.content.Context; import android.util.AttributeSet; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.LockPatternChecker; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockPatternView; @@ -34,7 +35,7 @@ import java.util.List; */ public class AuthCredentialPatternView extends AuthCredentialView { - private LockPatternView mLockPatternView; + @VisibleForTesting LockPatternView mLockPatternView; private class UnlockPatternListener implements LockPatternView.OnPatternListener { @@ -93,9 +94,7 @@ public class AuthCredentialPatternView extends AuthCredentialView { @Override protected void onErrorTimeoutFinish() { super.onErrorTimeoutFinish(); - // select to enable marquee unless a screen reader is enabled - mLockPatternView.setEnabled(!mAccessibilityManager.isEnabled() - || !mAccessibilityManager.isTouchExplorationEnabled()); + mLockPatternView.setEnabled(true); } public AuthCredentialPatternView(Context context, AttributeSet attrs) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java index 5958e6a436f1..157f14fb4c51 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java @@ -47,6 +47,7 @@ import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.StringRes; +import androidx.annotation.VisibleForTesting; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.VerifyCredentialResponse; @@ -98,7 +99,7 @@ public abstract class AuthCredentialView extends LinearLayout { protected int mUserId; protected long mOperationId; protected int mEffectiveUserId; - protected ErrorTimer mErrorTimer; + @VisibleForTesting ErrorTimer mErrorTimer; protected @Background DelayableExecutor mBackgroundExecutor; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 96fe65f8fd40..65fcd760360c 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -60,7 +60,9 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.doze.DozeReceiver; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.ScreenLifecycle; +import com.android.systemui.keyguard.domain.interactor.BouncerInteractor; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.ShadeExpansionStateManager; @@ -119,6 +121,7 @@ public class UdfpsController implements DozeReceiver { @NonNull private final SystemUIDialogManager mDialogManager; @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; @NonNull private final VibratorHelper mVibrator; + @NonNull private final FeatureFlags mFeatureFlags; @NonNull private final FalsingManager mFalsingManager; @NonNull private final PowerManager mPowerManager; @NonNull private final AccessibilityManager mAccessibilityManager; @@ -130,6 +133,7 @@ public class UdfpsController implements DozeReceiver { @NonNull private final LatencyTracker mLatencyTracker; @VisibleForTesting @NonNull final BiometricDisplayListener mOrientationListener; @NonNull private final ActivityLaunchAnimator mActivityLaunchAnimator; + @NonNull private final BouncerInteractor mBouncerInteractor; // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple // sensors, this, in addition to a lot of the code here, will be updated. @@ -212,7 +216,8 @@ public class UdfpsController implements DozeReceiver { mUnlockedScreenOffAnimationController, mUdfpsDisplayMode, requestId, reason, callback, (view, event, fromUdfpsView) -> onTouch(requestId, event, - fromUdfpsView), mActivityLaunchAnimator))); + fromUdfpsView), mActivityLaunchAnimator, mFeatureFlags, + mBouncerInteractor))); } @Override @@ -590,6 +595,7 @@ public class UdfpsController implements DozeReceiver { @NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager, @NonNull DumpManager dumpManager, @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor, + @NonNull FeatureFlags featureFlags, @NonNull FalsingManager falsingManager, @NonNull PowerManager powerManager, @NonNull AccessibilityManager accessibilityManager, @@ -608,7 +614,8 @@ public class UdfpsController implements DozeReceiver { @NonNull LatencyTracker latencyTracker, @NonNull ActivityLaunchAnimator activityLaunchAnimator, @NonNull Optional<AlternateUdfpsTouchProvider> alternateTouchProvider, - @BiometricsBackground Executor biometricsExecutor) { + @BiometricsBackground Executor biometricsExecutor, + @NonNull BouncerInteractor bouncerInteractor) { mContext = context; mExecution = execution; mVibrator = vibrator; @@ -638,6 +645,8 @@ public class UdfpsController implements DozeReceiver { mActivityLaunchAnimator = activityLaunchAnimator; mAlternateTouchProvider = alternateTouchProvider.orElse(null); mBiometricExecutor = biometricsExecutor; + mFeatureFlags = featureFlags; + mBouncerInteractor = bouncerInteractor; mOrientationListener = new BiometricDisplayListener( context, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index 7d0109686351..d70861ac5f19 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -48,6 +48,8 @@ import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.R import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.keyguard.domain.interactor.BouncerInteractor import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.LockscreenShadeTransitionController @@ -70,29 +72,31 @@ const val SETTING_REMOVE_ENROLLMENT_UI = "udfps_overlay_remove_enrollment_ui" */ @UiThread class UdfpsControllerOverlay @JvmOverloads constructor( - private val context: Context, - fingerprintManager: FingerprintManager, - private val inflater: LayoutInflater, - private val windowManager: WindowManager, - private val accessibilityManager: AccessibilityManager, - private val statusBarStateController: StatusBarStateController, - private val shadeExpansionStateManager: ShadeExpansionStateManager, - private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager, - private val keyguardUpdateMonitor: KeyguardUpdateMonitor, - private val dialogManager: SystemUIDialogManager, - private val dumpManager: DumpManager, - private val transitionController: LockscreenShadeTransitionController, - private val configurationController: ConfigurationController, - private val systemClock: SystemClock, - private val keyguardStateController: KeyguardStateController, - private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController, - private var udfpsDisplayModeProvider: UdfpsDisplayModeProvider, - val requestId: Long, - @ShowReason val requestReason: Int, - private val controllerCallback: IUdfpsOverlayControllerCallback, - private val onTouch: (View, MotionEvent, Boolean) -> Boolean, - private val activityLaunchAnimator: ActivityLaunchAnimator, - private val isDebuggable: Boolean = Build.IS_DEBUGGABLE + private val context: Context, + fingerprintManager: FingerprintManager, + private val inflater: LayoutInflater, + private val windowManager: WindowManager, + private val accessibilityManager: AccessibilityManager, + private val statusBarStateController: StatusBarStateController, + private val shadeExpansionStateManager: ShadeExpansionStateManager, + private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val dialogManager: SystemUIDialogManager, + private val dumpManager: DumpManager, + private val transitionController: LockscreenShadeTransitionController, + private val configurationController: ConfigurationController, + private val systemClock: SystemClock, + private val keyguardStateController: KeyguardStateController, + private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController, + private var udfpsDisplayModeProvider: UdfpsDisplayModeProvider, + val requestId: Long, + @ShowReason val requestReason: Int, + private val controllerCallback: IUdfpsOverlayControllerCallback, + private val onTouch: (View, MotionEvent, Boolean) -> Boolean, + private val activityLaunchAnimator: ActivityLaunchAnimator, + private val featureFlags: FeatureFlags, + private val bouncerInteractor: BouncerInteractor, + private val isDebuggable: Boolean = Build.IS_DEBUGGABLE ) { /** The view, when [isShowing], or null. */ var overlayView: UdfpsView? = null @@ -246,7 +250,9 @@ class UdfpsControllerOverlay @JvmOverloads constructor( unlockedScreenOffAnimationController, dialogManager, controller, - activityLaunchAnimator + activityLaunchAnimator, + featureFlags, + bouncerInteractor ) } REASON_AUTH_BP -> { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java deleted file mode 100644 index 4d7f89d7b727..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java +++ /dev/null @@ -1,548 +0,0 @@ -/* - * 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. - */ - -package com.android.systemui.biometrics; - -import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; - -import android.animation.ValueAnimator; -import android.annotation.NonNull; -import android.content.res.Configuration; -import android.util.MathUtils; -import android.view.MotionEvent; - -import com.android.keyguard.BouncerPanelExpansionCalculator; -import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.systemui.R; -import com.android.systemui.animation.ActivityLaunchAnimator; -import com.android.systemui.animation.Interpolators; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.shade.ShadeExpansionChangeEvent; -import com.android.systemui.shade.ShadeExpansionListener; -import com.android.systemui.shade.ShadeExpansionStateManager; -import com.android.systemui.statusbar.LockscreenShadeTransitionController; -import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.notification.stack.StackStateAnimator; -import com.android.systemui.statusbar.phone.KeyguardBouncer; -import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; -import com.android.systemui.statusbar.phone.SystemUIDialogManager; -import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController; -import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.systemui.statusbar.policy.KeyguardStateController; -import com.android.systemui.util.time.SystemClock; - -import java.io.PrintWriter; - -/** - * Class that coordinates non-HBM animations during keyguard authentication. - */ -public class UdfpsKeyguardViewController extends UdfpsAnimationViewController<UdfpsKeyguardView> { - public static final String TAG = "UdfpsKeyguardViewCtrl"; - @NonNull private final StatusBarKeyguardViewManager mKeyguardViewManager; - @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; - @NonNull private final LockscreenShadeTransitionController mLockScreenShadeTransitionController; - @NonNull private final ConfigurationController mConfigurationController; - @NonNull private final SystemClock mSystemClock; - @NonNull private final KeyguardStateController mKeyguardStateController; - @NonNull private final UdfpsController mUdfpsController; - @NonNull private final UnlockedScreenOffAnimationController - mUnlockedScreenOffAnimationController; - @NonNull private final ActivityLaunchAnimator mActivityLaunchAnimator; - private final ValueAnimator mUnlockedScreenOffDozeAnimator = ValueAnimator.ofFloat(0f, 1f); - - private boolean mShowingUdfpsBouncer; - private boolean mUdfpsRequested; - private float mQsExpansion; - private boolean mFaceDetectRunning; - private int mStatusBarState; - private float mTransitionToFullShadeProgress; - private float mLastDozeAmount; - private long mLastUdfpsBouncerShowTime = -1; - private float mPanelExpansionFraction; - private boolean mLaunchTransitionFadingAway; - private boolean mIsLaunchingActivity; - private float mActivityLaunchProgress; - - /** - * hidden amount of pin/pattern/password bouncer - * {@link KeyguardBouncer#EXPANSION_VISIBLE} (0f) to - * {@link KeyguardBouncer#EXPANSION_HIDDEN} (1f) - */ - private float mInputBouncerHiddenAmount; - private boolean mIsGenericBouncerShowing; // whether UDFPS bouncer or input bouncer is visible - - protected UdfpsKeyguardViewController( - @NonNull UdfpsKeyguardView view, - @NonNull StatusBarStateController statusBarStateController, - @NonNull ShadeExpansionStateManager shadeExpansionStateManager, - @NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager, - @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor, - @NonNull DumpManager dumpManager, - @NonNull LockscreenShadeTransitionController transitionController, - @NonNull ConfigurationController configurationController, - @NonNull SystemClock systemClock, - @NonNull KeyguardStateController keyguardStateController, - @NonNull UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, - @NonNull SystemUIDialogManager systemUIDialogManager, - @NonNull UdfpsController udfpsController, - @NonNull ActivityLaunchAnimator activityLaunchAnimator) { - super(view, statusBarStateController, shadeExpansionStateManager, systemUIDialogManager, - dumpManager); - mKeyguardViewManager = statusBarKeyguardViewManager; - mKeyguardUpdateMonitor = keyguardUpdateMonitor; - mLockScreenShadeTransitionController = transitionController; - mConfigurationController = configurationController; - mSystemClock = systemClock; - mKeyguardStateController = keyguardStateController; - mUdfpsController = udfpsController; - mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController; - mActivityLaunchAnimator = activityLaunchAnimator; - - mUnlockedScreenOffDozeAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); - mUnlockedScreenOffDozeAnimator.setInterpolator(Interpolators.ALPHA_IN); - mUnlockedScreenOffDozeAnimator.addUpdateListener( - new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - mView.onDozeAmountChanged( - animation.getAnimatedFraction(), - (float) animation.getAnimatedValue(), - UdfpsKeyguardView.ANIMATION_UNLOCKED_SCREEN_OFF); - } - }); - } - - @Override - @NonNull protected String getTag() { - return "UdfpsKeyguardViewController"; - } - - @Override - public void onInit() { - super.onInit(); - mKeyguardViewManager.setAlternateAuthInterceptor(mAlternateAuthInterceptor); - } - - @Override - protected void onViewAttached() { - super.onViewAttached(); - final float dozeAmount = getStatusBarStateController().getDozeAmount(); - mLastDozeAmount = dozeAmount; - mStateListener.onDozeAmountChanged(dozeAmount, dozeAmount); - getStatusBarStateController().addCallback(mStateListener); - - mUdfpsRequested = false; - - mLaunchTransitionFadingAway = mKeyguardStateController.isLaunchTransitionFadingAway(); - mKeyguardStateController.addCallback(mKeyguardStateControllerCallback); - mStatusBarState = getStatusBarStateController().getState(); - mQsExpansion = mKeyguardViewManager.getQsExpansion(); - updateGenericBouncerVisibility(); - mConfigurationController.addCallback(mConfigurationListener); - getShadeExpansionStateManager().addExpansionListener(mShadeExpansionListener); - updateScaleFactor(); - mView.updatePadding(); - updateAlpha(); - updatePauseAuth(); - - mKeyguardViewManager.setAlternateAuthInterceptor(mAlternateAuthInterceptor); - mLockScreenShadeTransitionController.setUdfpsKeyguardViewController(this); - mActivityLaunchAnimator.addListener(mActivityLaunchAnimatorListener); - } - - @Override - protected void onViewDetached() { - super.onViewDetached(); - mFaceDetectRunning = false; - - mKeyguardStateController.removeCallback(mKeyguardStateControllerCallback); - getStatusBarStateController().removeCallback(mStateListener); - mKeyguardViewManager.removeAlternateAuthInterceptor(mAlternateAuthInterceptor); - mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false); - mConfigurationController.removeCallback(mConfigurationListener); - getShadeExpansionStateManager().removeExpansionListener(mShadeExpansionListener); - if (mLockScreenShadeTransitionController.getUdfpsKeyguardViewController() == this) { - mLockScreenShadeTransitionController.setUdfpsKeyguardViewController(null); - } - mActivityLaunchAnimator.removeListener(mActivityLaunchAnimatorListener); - } - - @Override - public void dump(PrintWriter pw, String[] args) { - super.dump(pw, args); - pw.println("mShowingUdfpsBouncer=" + mShowingUdfpsBouncer); - pw.println("mFaceDetectRunning=" + mFaceDetectRunning); - pw.println("mStatusBarState=" + StatusBarState.toString(mStatusBarState)); - pw.println("mTransitionToFullShadeProgress=" + mTransitionToFullShadeProgress); - pw.println("mQsExpansion=" + mQsExpansion); - pw.println("mIsGenericBouncerShowing=" + mIsGenericBouncerShowing); - pw.println("mInputBouncerHiddenAmount=" + mInputBouncerHiddenAmount); - pw.println("mPanelExpansionFraction=" + mPanelExpansionFraction); - pw.println("unpausedAlpha=" + mView.getUnpausedAlpha()); - pw.println("mUdfpsRequested=" + mUdfpsRequested); - pw.println("mLaunchTransitionFadingAway=" + mLaunchTransitionFadingAway); - pw.println("mLastDozeAmount=" + mLastDozeAmount); - - mView.dump(pw); - } - - /** - * Overrides non-bouncer show logic in shouldPauseAuth to still show icon. - * @return whether the udfpsBouncer has been newly shown or hidden - */ - private boolean showUdfpsBouncer(boolean show) { - if (mShowingUdfpsBouncer == show) { - return false; - } - - boolean udfpsAffordanceWasNotShowing = shouldPauseAuth(); - mShowingUdfpsBouncer = show; - if (mShowingUdfpsBouncer) { - mLastUdfpsBouncerShowTime = mSystemClock.uptimeMillis(); - } - if (mShowingUdfpsBouncer) { - if (udfpsAffordanceWasNotShowing) { - mView.animateInUdfpsBouncer(null); - } - - if (mKeyguardStateController.isOccluded()) { - mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(true); - } - - mView.announceForAccessibility(mView.getContext().getString( - R.string.accessibility_fingerprint_bouncer)); - } else { - mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false); - } - - updateGenericBouncerVisibility(); - updateAlpha(); - updatePauseAuth(); - return true; - } - - /** - * Returns true if the fingerprint manager is running but we want to temporarily pause - * authentication. On the keyguard, we may want to show udfps when the shade - * is expanded, so this can be overridden with the showBouncer method. - */ - public boolean shouldPauseAuth() { - if (mShowingUdfpsBouncer) { - return false; - } - - if (mUdfpsRequested && !getNotificationShadeVisible() - && (!mIsGenericBouncerShowing - || mInputBouncerHiddenAmount != KeyguardBouncer.EXPANSION_VISIBLE) - && mKeyguardStateController.isShowing()) { - return false; - } - - if (mLaunchTransitionFadingAway) { - return true; - } - - // Only pause auth if we're not on the keyguard AND we're not transitioning to doze - // (ie: dozeAmount = 0f). For the UnlockedScreenOffAnimation, the statusBarState is - // delayed. However, we still animate in the UDFPS affordance with the - // mUnlockedScreenOffDozeAnimator. - if (mStatusBarState != KEYGUARD && mLastDozeAmount == 0f) { - return true; - } - - if (mInputBouncerHiddenAmount < .5f) { - return true; - } - - if (mView.getUnpausedAlpha() < (255 * .1)) { - return true; - } - - return false; - } - - @Override - public boolean listenForTouchesOutsideView() { - return true; - } - - @Override - public void onTouchOutsideView() { - maybeShowInputBouncer(); - } - - /** - * If we were previously showing the udfps bouncer, hide it and instead show the regular - * (pin/pattern/password) bouncer. - * - * Does nothing if we weren't previously showing the UDFPS bouncer. - */ - private void maybeShowInputBouncer() { - if (mShowingUdfpsBouncer && hasUdfpsBouncerShownWithMinTime()) { - mKeyguardViewManager.showBouncer(true); - } - } - - /** - * Whether the udfps bouncer has shown for at least 200ms before allowing touches outside - * of the udfps icon area to dismiss the udfps bouncer and show the pin/pattern/password - * bouncer. - */ - private boolean hasUdfpsBouncerShownWithMinTime() { - return (mSystemClock.uptimeMillis() - mLastUdfpsBouncerShowTime) > 200; - } - - /** - * Set the progress we're currently transitioning to the full shade. 0.0f means we're not - * transitioning yet, while 1.0f means we've fully dragged down. - * - * For example, start swiping down to expand the notification shade from the empty space in - * the middle of the lock screen. - */ - public void setTransitionToFullShadeProgress(float progress) { - mTransitionToFullShadeProgress = progress; - updateAlpha(); - } - - /** - * Update alpha for the UDFPS lock screen affordance. The AoD UDFPS visual affordance's - * alpha is based on the doze amount. - */ - @Override - public void updateAlpha() { - // Fade icon on transitions to showing the status bar or bouncer, but if mUdfpsRequested, - // then the keyguard is occluded by some application - so instead use the input bouncer - // hidden amount to determine the fade. - float expansion = mUdfpsRequested ? mInputBouncerHiddenAmount : mPanelExpansionFraction; - - int alpha = mShowingUdfpsBouncer ? 255 - : (int) MathUtils.constrain( - MathUtils.map(.5f, .9f, 0f, 255f, expansion), - 0f, 255f); - - if (!mShowingUdfpsBouncer) { - // swipe from top of the lockscreen to expand full QS: - alpha *= (1.0f - Interpolators.EMPHASIZED_DECELERATE.getInterpolation(mQsExpansion)); - - // swipe from the middle (empty space) of lockscreen to expand the notification shade: - alpha *= (1.0f - mTransitionToFullShadeProgress); - - // Fade out the icon if we are animating an activity launch over the lockscreen and the - // activity didn't request the UDFPS. - if (mIsLaunchingActivity && !mUdfpsRequested) { - alpha *= (1.0f - mActivityLaunchProgress); - } - - // Fade out alpha when a dialog is shown - // Fade in alpha when a dialog is hidden - alpha *= mView.getDialogSuggestedAlpha(); - } - mView.setUnpausedAlpha(alpha); - } - - /** - * Updates mIsGenericBouncerShowing (whether any bouncer is showing) and updates the - * mInputBouncerHiddenAmount to reflect whether the input bouncer is fully showing or not. - */ - private void updateGenericBouncerVisibility() { - mIsGenericBouncerShowing = mKeyguardViewManager.isBouncerShowing(); // includes altBouncer - final boolean altBouncerShowing = mKeyguardViewManager.isShowingAlternateAuth(); - if (altBouncerShowing || !mKeyguardViewManager.bouncerIsOrWillBeShowing()) { - mInputBouncerHiddenAmount = 1f; - } else if (mIsGenericBouncerShowing) { - // input bouncer is fully showing - mInputBouncerHiddenAmount = 0f; - } - } - - /** - * Update the scale factor based on the device's resolution. - */ - private void updateScaleFactor() { - if (mUdfpsController != null && mUdfpsController.mOverlayParams != null) { - mView.setScaleFactor(mUdfpsController.mOverlayParams.getScaleFactor()); - } - } - - private final StatusBarStateController.StateListener mStateListener = - new StatusBarStateController.StateListener() { - @Override - public void onDozeAmountChanged(float linear, float eased) { - if (mLastDozeAmount < linear) { - showUdfpsBouncer(false); - } - mUnlockedScreenOffDozeAnimator.cancel(); - final boolean animatingFromUnlockedScreenOff = - mUnlockedScreenOffAnimationController.isAnimationPlaying(); - if (animatingFromUnlockedScreenOff && linear != 0f) { - // we manually animate the fade in of the UDFPS icon since the unlocked - // screen off animation prevents the doze amounts to be incrementally eased in - mUnlockedScreenOffDozeAnimator.start(); - } else { - mView.onDozeAmountChanged(linear, eased, - UdfpsKeyguardView.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN); - } - - mLastDozeAmount = linear; - updatePauseAuth(); - } - - @Override - public void onStateChanged(int statusBarState) { - mStatusBarState = statusBarState; - updateAlpha(); - updatePauseAuth(); - } - }; - - private final StatusBarKeyguardViewManager.AlternateAuthInterceptor mAlternateAuthInterceptor = - new StatusBarKeyguardViewManager.AlternateAuthInterceptor() { - @Override - public boolean showAlternateAuthBouncer() { - return showUdfpsBouncer(true); - } - - @Override - public boolean hideAlternateAuthBouncer() { - return showUdfpsBouncer(false); - } - - @Override - public boolean isShowingAlternateAuthBouncer() { - return mShowingUdfpsBouncer; - } - - @Override - public void requestUdfps(boolean request, int color) { - mUdfpsRequested = request; - mView.requestUdfps(request, color); - updateAlpha(); - updatePauseAuth(); - } - - @Override - public boolean isAnimating() { - return false; - } - - /** - * Set the amount qs is expanded. Forxample, swipe down from the top of the - * lock screen to start the full QS expansion. - */ - @Override - public void setQsExpansion(float qsExpansion) { - mQsExpansion = qsExpansion; - updateAlpha(); - updatePauseAuth(); - } - - @Override - public boolean onTouch(MotionEvent event) { - if (mTransitionToFullShadeProgress != 0) { - return false; - } - return mUdfpsController.onTouch(event); - } - - @Override - public void setBouncerExpansionChanged(float expansion) { - mInputBouncerHiddenAmount = expansion; - updateAlpha(); - updatePauseAuth(); - } - - /** - * Only called on primary auth bouncer changes, not on whether the UDFPS bouncer - * visibility changes. - */ - @Override - public void onBouncerVisibilityChanged() { - updateGenericBouncerVisibility(); - updateAlpha(); - updatePauseAuth(); - } - - @Override - public void dump(PrintWriter pw) { - pw.println(getTag()); - } - }; - - private final ConfigurationController.ConfigurationListener mConfigurationListener = - new ConfigurationController.ConfigurationListener() { - @Override - public void onUiModeChanged() { - mView.updateColor(); - } - - @Override - public void onThemeChanged() { - mView.updateColor(); - } - - @Override - public void onConfigChanged(Configuration newConfig) { - updateScaleFactor(); - mView.updatePadding(); - mView.updateColor(); - } - }; - - private final ShadeExpansionListener mShadeExpansionListener = new ShadeExpansionListener() { - @Override - public void onPanelExpansionChanged(ShadeExpansionChangeEvent event) { - float fraction = event.getFraction(); - mPanelExpansionFraction = - mKeyguardViewManager.isBouncerInTransit() ? BouncerPanelExpansionCalculator - .aboutToShowBouncerProgress(fraction) : fraction; - updateAlpha(); - updatePauseAuth(); - } - }; - - private final KeyguardStateController.Callback mKeyguardStateControllerCallback = - new KeyguardStateController.Callback() { - @Override - public void onLaunchTransitionFadingAwayChanged() { - mLaunchTransitionFadingAway = - mKeyguardStateController.isLaunchTransitionFadingAway(); - updatePauseAuth(); - } - }; - - private final ActivityLaunchAnimator.Listener mActivityLaunchAnimatorListener = - new ActivityLaunchAnimator.Listener() { - @Override - public void onLaunchAnimationStart() { - mIsLaunchingActivity = true; - mActivityLaunchProgress = 0f; - updateAlpha(); - } - - @Override - public void onLaunchAnimationEnd() { - mIsLaunchingActivity = false; - updateAlpha(); - } - - @Override - public void onLaunchAnimationProgress(float linearProgress) { - mActivityLaunchProgress = linearProgress; - updateAlpha(); - } - }; -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt new file mode 100644 index 000000000000..5bae2dc502d6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt @@ -0,0 +1,550 @@ +/* + * 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. + */ + +package com.android.systemui.biometrics + +import android.animation.ValueAnimator +import android.content.res.Configuration +import android.util.MathUtils +import android.view.MotionEvent +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.R +import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.Interpolators +import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.domain.interactor.BouncerInteractor +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionListener +import com.android.systemui.shade.ShadeExpansionStateManager +import com.android.systemui.statusbar.LockscreenShadeTransitionController +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.notification.stack.StackStateAnimator +import com.android.systemui.statusbar.phone.KeyguardBouncer +import com.android.systemui.statusbar.phone.KeyguardBouncer.BouncerExpansionCallback +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.AlternateAuthInterceptor +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.KeyguardViewManagerCallback +import com.android.systemui.statusbar.phone.SystemUIDialogManager +import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.time.SystemClock +import java.io.PrintWriter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +/** Class that coordinates non-HBM animations during keyguard authentication. */ +open class UdfpsKeyguardViewController +constructor( + private val view: UdfpsKeyguardView, + statusBarStateController: StatusBarStateController, + shadeExpansionStateManager: ShadeExpansionStateManager, + private val keyguardViewManager: StatusBarKeyguardViewManager, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + dumpManager: DumpManager, + private val lockScreenShadeTransitionController: LockscreenShadeTransitionController, + private val configurationController: ConfigurationController, + private val systemClock: SystemClock, + private val keyguardStateController: KeyguardStateController, + private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController, + systemUIDialogManager: SystemUIDialogManager, + private val udfpsController: UdfpsController, + private val activityLaunchAnimator: ActivityLaunchAnimator, + featureFlags: FeatureFlags, + private val bouncerInteractor: BouncerInteractor +) : + UdfpsAnimationViewController<UdfpsKeyguardView>( + view, + statusBarStateController, + shadeExpansionStateManager, + systemUIDialogManager, + dumpManager + ) { + private val isModernBouncerEnabled: Boolean = featureFlags.isEnabled(Flags.MODERN_BOUNCER) + private var showingUdfpsBouncer = false + private var udfpsRequested = false + private var qsExpansion = 0f + private var faceDetectRunning = false + private var statusBarState = 0 + private var transitionToFullShadeProgress = 0f + private var lastDozeAmount = 0f + private var lastUdfpsBouncerShowTime: Long = -1 + private var panelExpansionFraction = 0f + private var launchTransitionFadingAway = false + private var isLaunchingActivity = false + private var activityLaunchProgress = 0f + private val unlockedScreenOffDozeAnimator = + ValueAnimator.ofFloat(0f, 1f).apply { + duration = StackStateAnimator.ANIMATION_DURATION_STANDARD.toLong() + interpolator = Interpolators.ALPHA_IN + addUpdateListener { animation -> + view.onDozeAmountChanged( + animation.animatedFraction, + animation.animatedValue as Float, + UdfpsKeyguardView.ANIMATION_UNLOCKED_SCREEN_OFF + ) + } + } + /** + * Hidden amount of input (pin/pattern/password) bouncer. This is used + * [KeyguardBouncer.EXPANSION_VISIBLE] (0f) to [KeyguardBouncer.EXPANSION_HIDDEN] (1f). Only + * used for the non-modernBouncer. + */ + private var inputBouncerHiddenAmount = KeyguardBouncer.EXPANSION_HIDDEN + private var inputBouncerExpansion = 0f // only used for modernBouncer + + private val stateListener: StatusBarStateController.StateListener = + object : StatusBarStateController.StateListener { + override fun onDozeAmountChanged(linear: Float, eased: Float) { + if (lastDozeAmount < linear) { + showUdfpsBouncer(false) + } + unlockedScreenOffDozeAnimator.cancel() + val animatingFromUnlockedScreenOff = + unlockedScreenOffAnimationController.isAnimationPlaying() + if (animatingFromUnlockedScreenOff && linear != 0f) { + // we manually animate the fade in of the UDFPS icon since the unlocked + // screen off animation prevents the doze amounts to be incrementally eased in + unlockedScreenOffDozeAnimator.start() + } else { + view.onDozeAmountChanged( + linear, + eased, + UdfpsKeyguardView.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN + ) + } + lastDozeAmount = linear + updatePauseAuth() + } + + override fun onStateChanged(statusBarState: Int) { + this@UdfpsKeyguardViewController.statusBarState = statusBarState + updateAlpha() + updatePauseAuth() + } + } + + private val bouncerExpansionCallback: BouncerExpansionCallback = + object : BouncerExpansionCallback { + override fun onExpansionChanged(expansion: Float) { + inputBouncerHiddenAmount = expansion + updateAlpha() + updatePauseAuth() + } + + override fun onVisibilityChanged(isVisible: Boolean) { + updateBouncerHiddenAmount() + updateAlpha() + updatePauseAuth() + } + } + + private val configurationListener: ConfigurationController.ConfigurationListener = + object : ConfigurationController.ConfigurationListener { + override fun onUiModeChanged() { + view.updateColor() + } + + override fun onThemeChanged() { + view.updateColor() + } + + override fun onConfigChanged(newConfig: Configuration) { + updateScaleFactor() + view.updatePadding() + view.updateColor() + } + } + + private val shadeExpansionListener = ShadeExpansionListener { (fraction) -> + panelExpansionFraction = + if (keyguardViewManager.isBouncerInTransit) { + aboutToShowBouncerProgress(fraction) + } else { + fraction + } + updateAlpha() + updatePauseAuth() + } + + private val keyguardStateControllerCallback: KeyguardStateController.Callback = + object : KeyguardStateController.Callback { + override fun onLaunchTransitionFadingAwayChanged() { + launchTransitionFadingAway = keyguardStateController.isLaunchTransitionFadingAway + updatePauseAuth() + } + } + + private val activityLaunchAnimatorListener: ActivityLaunchAnimator.Listener = + object : ActivityLaunchAnimator.Listener { + override fun onLaunchAnimationStart() { + isLaunchingActivity = true + activityLaunchProgress = 0f + updateAlpha() + } + + override fun onLaunchAnimationEnd() { + isLaunchingActivity = false + updateAlpha() + } + + override fun onLaunchAnimationProgress(linearProgress: Float) { + activityLaunchProgress = linearProgress + updateAlpha() + } + } + + private val statusBarKeyguardViewManagerCallback: KeyguardViewManagerCallback = + object : KeyguardViewManagerCallback { + override fun onQSExpansionChanged(qsExpansion: Float) { + this@UdfpsKeyguardViewController.qsExpansion = qsExpansion + updateAlpha() + updatePauseAuth() + } + + /** + * Forward touches to the UdfpsController. This allows the touch to start from outside + * the sensor area and then slide their finger into the sensor area. + */ + override fun onTouch(event: MotionEvent) { + // Don't forward touches if the shade has already started expanding. + if (transitionToFullShadeProgress != 0f) { + return + } + udfpsController.onTouch(event) + } + } + + private val alternateAuthInterceptor: AlternateAuthInterceptor = + object : AlternateAuthInterceptor { + override fun showAlternateAuthBouncer(): Boolean { + return showUdfpsBouncer(true) + } + + override fun hideAlternateAuthBouncer(): Boolean { + return showUdfpsBouncer(false) + } + + override fun isShowingAlternateAuthBouncer(): Boolean { + return showingUdfpsBouncer + } + + override fun requestUdfps(request: Boolean, color: Int) { + udfpsRequested = request + view.requestUdfps(request, color) + updateAlpha() + updatePauseAuth() + } + + override fun dump(pw: PrintWriter) { + pw.println(tag) + } + } + + override val tag: String + get() = TAG + + override fun onInit() { + super.onInit() + keyguardViewManager.setAlternateAuthInterceptor(alternateAuthInterceptor) + } + + init { + if (isModernBouncerEnabled) { + view.repeatWhenAttached { + // repeatOnLifecycle CREATED (as opposed to STARTED) because the Bouncer expansion + // can make the view not visible; and we still want to listen for events + // that may make the view visible again. + repeatOnLifecycle(Lifecycle.State.CREATED) { listenForBouncerExpansion(this) } + } + } + } + + @VisibleForTesting + internal suspend fun listenForBouncerExpansion(scope: CoroutineScope): Job { + return scope.launch { + bouncerInteractor.bouncerExpansion.collect { bouncerExpansion: Float -> + inputBouncerExpansion = bouncerExpansion + updateAlpha() + updatePauseAuth() + } + } + } + + public override fun onViewAttached() { + super.onViewAttached() + val dozeAmount = statusBarStateController.dozeAmount + lastDozeAmount = dozeAmount + stateListener.onDozeAmountChanged(dozeAmount, dozeAmount) + statusBarStateController.addCallback(stateListener) + udfpsRequested = false + launchTransitionFadingAway = keyguardStateController.isLaunchTransitionFadingAway + keyguardStateController.addCallback(keyguardStateControllerCallback) + statusBarState = statusBarStateController.state + qsExpansion = keyguardViewManager.qsExpansion + keyguardViewManager.addCallback(statusBarKeyguardViewManagerCallback) + if (!isModernBouncerEnabled) { + val bouncer = keyguardViewManager.bouncer + bouncer?.expansion?.let { + bouncerExpansionCallback.onExpansionChanged(it) + bouncer.addBouncerExpansionCallback(bouncerExpansionCallback) + } + updateBouncerHiddenAmount() + } + configurationController.addCallback(configurationListener) + shadeExpansionStateManager.addExpansionListener(shadeExpansionListener) + updateScaleFactor() + view.updatePadding() + updateAlpha() + updatePauseAuth() + keyguardViewManager.setAlternateAuthInterceptor(alternateAuthInterceptor) + lockScreenShadeTransitionController.udfpsKeyguardViewController = this + activityLaunchAnimator.addListener(activityLaunchAnimatorListener) + } + + override fun onViewDetached() { + super.onViewDetached() + faceDetectRunning = false + keyguardStateController.removeCallback(keyguardStateControllerCallback) + statusBarStateController.removeCallback(stateListener) + keyguardViewManager.removeAlternateAuthInterceptor(alternateAuthInterceptor) + keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false) + configurationController.removeCallback(configurationListener) + shadeExpansionStateManager.removeExpansionListener(shadeExpansionListener) + if (lockScreenShadeTransitionController.udfpsKeyguardViewController === this) { + lockScreenShadeTransitionController.udfpsKeyguardViewController = null + } + activityLaunchAnimator.removeListener(activityLaunchAnimatorListener) + keyguardViewManager.removeCallback(statusBarKeyguardViewManagerCallback) + if (!isModernBouncerEnabled) { + keyguardViewManager.bouncer?.removeBouncerExpansionCallback(bouncerExpansionCallback) + } + } + + override fun dump(pw: PrintWriter, args: Array<String>) { + super.dump(pw, args) + pw.println("isModernBouncerEnabled=$isModernBouncerEnabled") + pw.println("showingUdfpsAltBouncer=$showingUdfpsBouncer") + pw.println("faceDetectRunning=$faceDetectRunning") + pw.println("statusBarState=" + StatusBarState.toString(statusBarState)) + pw.println("transitionToFullShadeProgress=$transitionToFullShadeProgress") + pw.println("qsExpansion=$qsExpansion") + pw.println("panelExpansionFraction=$panelExpansionFraction") + pw.println("unpausedAlpha=" + view.unpausedAlpha) + pw.println("udfpsRequestedByApp=$udfpsRequested") + pw.println("launchTransitionFadingAway=$launchTransitionFadingAway") + pw.println("lastDozeAmount=$lastDozeAmount") + if (isModernBouncerEnabled) { + pw.println("inputBouncerExpansion=$inputBouncerExpansion") + } else { + pw.println("inputBouncerHiddenAmount=$inputBouncerHiddenAmount") + } + view.dump(pw) + } + + /** + * Overrides non-bouncer show logic in shouldPauseAuth to still show icon. + * @return whether the udfpsBouncer has been newly shown or hidden + */ + private fun showUdfpsBouncer(show: Boolean): Boolean { + if (showingUdfpsBouncer == show) { + return false + } + val udfpsAffordanceWasNotShowing = shouldPauseAuth() + showingUdfpsBouncer = show + if (showingUdfpsBouncer) { + lastUdfpsBouncerShowTime = systemClock.uptimeMillis() + } + if (showingUdfpsBouncer) { + if (udfpsAffordanceWasNotShowing) { + view.animateInUdfpsBouncer(null) + } + if (keyguardStateController.isOccluded) { + keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(true) + } + view.announceForAccessibility( + view.context.getString(R.string.accessibility_fingerprint_bouncer) + ) + } else { + keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false) + } + updateBouncerHiddenAmount() + updateAlpha() + updatePauseAuth() + return true + } + + /** + * Returns true if the fingerprint manager is running but we want to temporarily pause + * authentication. On the keyguard, we may want to show udfps when the shade is expanded, so + * this can be overridden with the showBouncer method. + */ + override fun shouldPauseAuth(): Boolean { + if (showingUdfpsBouncer) { + return false + } + if ( + udfpsRequested && + !notificationShadeVisible && + !isInputBouncerFullyVisible() && + keyguardStateController.isShowing + ) { + return false + } + if (launchTransitionFadingAway) { + return true + } + + // Only pause auth if we're not on the keyguard AND we're not transitioning to doze + // (ie: dozeAmount = 0f). For the UnlockedScreenOffAnimation, the statusBarState is + // delayed. However, we still animate in the UDFPS affordance with the + // mUnlockedScreenOffDozeAnimator. + if (statusBarState != StatusBarState.KEYGUARD && lastDozeAmount == 0f) { + return true + } + if (isBouncerExpansionGreaterThan(.5f)) { + return true + } + return view.unpausedAlpha < 255 * .1 + } + + fun isBouncerExpansionGreaterThan(bouncerExpansionThreshold: Float): Boolean { + return if (isModernBouncerEnabled) { + inputBouncerExpansion >= bouncerExpansionThreshold + } else { + inputBouncerHiddenAmount < bouncerExpansionThreshold + } + } + + fun isInputBouncerFullyVisible(): Boolean { + return if (isModernBouncerEnabled) { + inputBouncerExpansion == 1f + } else { + keyguardViewManager.isBouncerShowing && !keyguardViewManager.isShowingAlternateAuth + } + } + + override fun listenForTouchesOutsideView(): Boolean { + return true + } + + override fun onTouchOutsideView() { + maybeShowInputBouncer() + } + + /** + * If we were previously showing the udfps bouncer, hide it and instead show the regular + * (pin/pattern/password) bouncer. + * + * Does nothing if we weren't previously showing the UDFPS bouncer. + */ + private fun maybeShowInputBouncer() { + if (showingUdfpsBouncer && hasUdfpsBouncerShownWithMinTime()) { + keyguardViewManager.showBouncer(true) + } + } + + /** + * Whether the udfps bouncer has shown for at least 200ms before allowing touches outside of the + * udfps icon area to dismiss the udfps bouncer and show the pin/pattern/password bouncer. + */ + private fun hasUdfpsBouncerShownWithMinTime(): Boolean { + return systemClock.uptimeMillis() - lastUdfpsBouncerShowTime > 200 + } + + /** + * Set the progress we're currently transitioning to the full shade. 0.0f means we're not + * transitioning yet, while 1.0f means we've fully dragged down. For example, start swiping down + * to expand the notification shade from the empty space in the middle of the lock screen. + */ + fun setTransitionToFullShadeProgress(progress: Float) { + transitionToFullShadeProgress = progress + updateAlpha() + } + + /** + * Update alpha for the UDFPS lock screen affordance. The AoD UDFPS visual affordance's alpha is + * based on the doze amount. + */ + override fun updateAlpha() { + // Fade icon on transitions to showing the status bar or bouncer, but if mUdfpsRequested, + // then the keyguard is occluded by some application - so instead use the input bouncer + // hidden amount to determine the fade. + val expansion = if (udfpsRequested) getInputBouncerHiddenAmt() else panelExpansionFraction + var alpha: Int = + if (showingUdfpsBouncer) 255 + else MathUtils.constrain(MathUtils.map(.5f, .9f, 0f, 255f, expansion), 0f, 255f).toInt() + if (!showingUdfpsBouncer) { + // swipe from top of the lockscreen to expand full QS: + alpha = + (alpha * (1.0f - Interpolators.EMPHASIZED_DECELERATE.getInterpolation(qsExpansion))) + .toInt() + + // swipe from the middle (empty space) of lockscreen to expand the notification shade: + alpha = (alpha * (1.0f - transitionToFullShadeProgress)).toInt() + + // Fade out the icon if we are animating an activity launch over the lockscreen and the + // activity didn't request the UDFPS. + if (isLaunchingActivity && !udfpsRequested) { + alpha = (alpha * (1.0f - activityLaunchProgress)).toInt() + } + + // Fade out alpha when a dialog is shown + // Fade in alpha when a dialog is hidden + alpha = (alpha * view.dialogSuggestedAlpha).toInt() + } + view.unpausedAlpha = alpha + } + + private fun getInputBouncerHiddenAmt(): Float { + return if (isModernBouncerEnabled) { + 1f - inputBouncerExpansion + } else { + inputBouncerHiddenAmount + } + } + + /** Update the scale factor based on the device's resolution. */ + private fun updateScaleFactor() { + udfpsController.mOverlayParams?.scaleFactor?.let { view.setScaleFactor(it) } + } + + private fun updateBouncerHiddenAmount() { + if (isModernBouncerEnabled) { + return + } + val altBouncerShowing = keyguardViewManager.isShowingAlternateAuth + if (altBouncerShowing || !keyguardViewManager.bouncerIsOrWillBeShowing()) { + inputBouncerHiddenAmount = 1f + } else if (keyguardViewManager.isBouncerShowing) { + // input bouncer is fully showing + inputBouncerHiddenAmount = 0f + } + } + + companion object { + const val TAG = "UdfpsKeyguardViewController" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 344fb76dca04..844a311a988b 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -232,11 +232,13 @@ object Flags { @JvmField val ROUNDED_BOX_RIPPLE = ReleasedFlag(1002) // 1100 - windowing + @JvmField @Keep val WM_ENABLE_SHELL_TRANSITIONS = SysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", false) /** b/170163464: animate bubbles expanded view collapse with home gesture */ + @JvmField @Keep val BUBBLES_HOME_GESTURE = SysPropBooleanFlag(1101, "persist.wm.debug.bubbles_home_gesture", true) @@ -258,40 +260,50 @@ object Flags { @Keep val HIDE_NAVBAR_WINDOW = SysPropBooleanFlag(1103, "persist.wm.debug.hide_navbar_window", false) + @JvmField @Keep val WM_DESKTOP_WINDOWING = SysPropBooleanFlag(1104, "persist.wm.debug.desktop_mode", false) + @JvmField @Keep val WM_CAPTION_ON_SHELL = SysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", false) + @JvmField @Keep val FLOATING_TASKS_ENABLED = SysPropBooleanFlag(1106, "persist.wm.debug.floating_tasks", false) + @JvmField @Keep val SHOW_FLOATING_TASKS_AS_BUBBLES = SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false) + @JvmField @Keep val ENABLE_FLING_TO_DISMISS_BUBBLE = SysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", true) + @JvmField @Keep val ENABLE_FLING_TO_DISMISS_PIP = SysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", true) + @JvmField @Keep val ENABLE_PIP_KEEP_CLEAR_ALGORITHM = SysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", false) // 1200 - predictive back + @JvmField @Keep val WM_ENABLE_PREDICTIVE_BACK = SysPropBooleanFlag(1200, "persist.wm.debug.predictive_back", true) + @JvmField @Keep val WM_ENABLE_PREDICTIVE_BACK_ANIM = SysPropBooleanFlag(1201, "persist.wm.debug.predictive_back_anim", false) + @JvmField @Keep val WM_ALWAYS_ENFORCE_PREDICTIVE_BACK = SysPropBooleanFlag(1202, "persist.wm.debug.predictive_back_always_enforce", false) @@ -301,7 +313,7 @@ object Flags { // 1300 - screenshots // TODO(b/254512719): Tracking Bug - @JvmField val SCREENSHOT_REQUEST_PROCESSOR = UnreleasedFlag(1300) + @JvmField val SCREENSHOT_REQUEST_PROCESSOR = UnreleasedFlag(1300, true) // TODO(b/254513155): Tracking Bug @JvmField val SCREENSHOT_WORK_PROFILE_POLICY = UnreleasedFlag(1301) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt index 99ae85d7a548..80c6130955c5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.data import android.view.KeyEvent import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.plugins.ActivityStarter import java.lang.ref.WeakReference import javax.inject.Inject @@ -45,4 +46,9 @@ interface BouncerViewDelegate { fun dispatchBackKeyEventPreIme(): Boolean fun showNextSecurityScreenOrFinish(): Boolean fun resume() + fun setDismissAction( + onDismissAction: ActivityStarter.OnDismissAction?, + cancelAction: Runnable?, + ) + fun willDismissWithActions(): Boolean } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt index 543389e0a7cd..0046256c677b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt @@ -21,10 +21,9 @@ import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.keyguard.ViewMediatorCallback import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.shared.model.BouncerCallbackActionsModel import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel -import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_HIDDEN +import com.android.systemui.statusbar.phone.KeyguardBouncer import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -41,9 +40,15 @@ constructor( /** Determines if we want to instantaneously show the bouncer instead of translating. */ private val _isScrimmed = MutableStateFlow(false) val isScrimmed = _isScrimmed.asStateFlow() - /** Set amount of how much of the bouncer is showing on the screen */ - private val _expansionAmount = MutableStateFlow(EXPANSION_HIDDEN) - val expansionAmount = _expansionAmount.asStateFlow() + /** + * Set how much of the panel is showing on the screen. + * ``` + * 0f = panel fully hidden = bouncer fully showing + * 1f = panel fully showing = bouncer fully hidden + * ``` + */ + private val _panelExpansionAmount = MutableStateFlow(KeyguardBouncer.EXPANSION_HIDDEN) + val panelExpansionAmount = _panelExpansionAmount.asStateFlow() private val _isVisible = MutableStateFlow(false) val isVisible = _isVisible.asStateFlow() private val _show = MutableStateFlow<KeyguardBouncerModel?>(null) @@ -54,8 +59,6 @@ constructor( val hide = _hide.asStateFlow() private val _startingToHide = MutableStateFlow(false) val startingToHide = _startingToHide.asStateFlow() - private val _onDismissAction = MutableStateFlow<BouncerCallbackActionsModel?>(null) - val onDismissAction = _onDismissAction.asStateFlow() private val _disappearAnimation = MutableStateFlow<Runnable?>(null) val startingDisappearAnimation = _disappearAnimation.asStateFlow() private val _keyguardPosition = MutableStateFlow(0f) @@ -96,8 +99,8 @@ constructor( _isScrimmed.value = isScrimmed } - fun setExpansion(expansion: Float) { - _expansionAmount.value = expansion + fun setPanelExpansion(panelExpansion: Float) { + _panelExpansionAmount.value = panelExpansion } fun setVisible(isVisible: Boolean) { @@ -120,10 +123,6 @@ constructor( _startingToHide.value = startingToHide } - fun setOnDismissAction(bouncerCallbackActionsModel: BouncerCallbackActionsModel?) { - _onDismissAction.value = bouncerCallbackActionsModel - } - fun setStartDisappearAnimation(runnable: Runnable?) { _disappearAnimation.value = runnable } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt index 2af9318d92ec..dbb0352c2187 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt @@ -30,7 +30,6 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.BouncerView import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository -import com.android.systemui.keyguard.shared.model.BouncerCallbackActionsModel import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel import com.android.systemui.plugins.ActivityStarter @@ -40,6 +39,7 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map @@ -77,7 +77,7 @@ constructor( KeyguardBouncerModel( promptReason = repository.bouncerPromptReason ?: 0, errorMessage = repository.bouncerErrorMessage, - expansionAmount = repository.expansionAmount.value + expansionAmount = repository.panelExpansionAmount.value ) ) repository.setShowingSoon(false) @@ -90,14 +90,22 @@ constructor( val startingToHide: Flow<Unit> = repository.startingToHide.filter { it }.map {} val isVisible: Flow<Boolean> = repository.isVisible val isBackButtonEnabled: Flow<Boolean> = repository.isBackButtonEnabled.filterNotNull() - val expansionAmount: Flow<Float> = repository.expansionAmount val showMessage: Flow<BouncerShowMessageModel> = repository.showMessage.filterNotNull() val startingDisappearAnimation: Flow<Runnable> = repository.startingDisappearAnimation.filterNotNull() - val onDismissAction: Flow<BouncerCallbackActionsModel> = - repository.onDismissAction.filterNotNull() val resourceUpdateRequests: Flow<Boolean> = repository.resourceUpdateRequests.filter { it } val keyguardPosition: Flow<Float> = repository.keyguardPosition + val panelExpansionAmount: Flow<Float> = repository.panelExpansionAmount + /** 0f = bouncer fully hidden. 1f = bouncer fully visible. */ + val bouncerExpansion: Flow<Float> = // + combine(repository.panelExpansionAmount, repository.isVisible) { expansionAmount, isVisible + -> + if (isVisible) { + 1f - expansionAmount + } else { + 0f + } + } // TODO(b/243685699): Move isScrimmed logic to data layer. // TODO(b/243695312): Encapsulate all of the show logic for the bouncer. @@ -128,7 +136,7 @@ constructor( Trace.beginSection("KeyguardBouncer#show") repository.setScrimmed(isScrimmed) if (isScrimmed) { - setExpansion(KeyguardBouncer.EXPANSION_VISIBLE) + setPanelExpansion(KeyguardBouncer.EXPANSION_VISIBLE) } if (resumeBouncer) { @@ -149,7 +157,6 @@ constructor( } keyguardStateController.notifyBouncerShowing(true) callbackInteractor.dispatchStartingToShow() - Trace.endSection() } @@ -168,7 +175,6 @@ constructor( keyguardStateController.notifyBouncerShowing(false /* showing */) cancelShowRunnable() repository.setShowingSoon(false) - repository.setOnDismissAction(null) repository.setVisible(false) repository.setHide(true) repository.setShow(null) @@ -176,14 +182,17 @@ constructor( } /** - * Sets the panel expansion which is calculated further upstream. Expansion is from 0f to 1f - * where 0f => showing and 1f => hiding + * Sets the panel expansion which is calculated further upstream. Panel expansion is from 0f + * (panel fully hidden) to 1f (panel fully showing). As the panel shows (from 0f => 1f), the + * bouncer hides and as the panel becomes hidden (1f => 0f), the bouncer starts to show. + * Therefore, a panel expansion of 1f represents the bouncer fully hidden and a panel expansion + * of 0f represents the bouncer fully showing. */ - fun setExpansion(expansion: Float) { - val oldExpansion = repository.expansionAmount.value + fun setPanelExpansion(expansion: Float) { + val oldExpansion = repository.panelExpansionAmount.value val expansionChanged = oldExpansion != expansion if (repository.startingDisappearAnimation.value == null) { - repository.setExpansion(expansion) + repository.setPanelExpansion(expansion) } if ( @@ -227,7 +236,7 @@ constructor( onDismissAction: ActivityStarter.OnDismissAction?, cancelAction: Runnable? ) { - repository.setOnDismissAction(BouncerCallbackActionsModel(onDismissAction, cancelAction)) + bouncerView.delegate?.setDismissAction(onDismissAction, cancelAction) } /** Update the resources of the views. */ @@ -282,7 +291,7 @@ constructor( /** Returns whether bouncer is fully showing. */ fun isFullyShowing(): Boolean { return (repository.showingSoon.value || repository.isVisible.value) && - repository.expansionAmount.value == KeyguardBouncer.EXPANSION_VISIBLE && + repository.panelExpansionAmount.value == KeyguardBouncer.EXPANSION_VISIBLE && repository.startingDisappearAnimation.value == null } @@ -294,8 +303,8 @@ constructor( /** If bouncer expansion is between 0f and 1f non-inclusive. */ fun isInTransit(): Boolean { return repository.showingSoon.value || - repository.expansionAmount.value != KeyguardBouncer.EXPANSION_HIDDEN && - repository.expansionAmount.value != KeyguardBouncer.EXPANSION_VISIBLE + repository.panelExpansionAmount.value != KeyguardBouncer.EXPANSION_HIDDEN && + repository.panelExpansionAmount.value != KeyguardBouncer.EXPANSION_VISIBLE } /** Return whether bouncer is animating away. */ @@ -305,7 +314,7 @@ constructor( /** Return whether bouncer will dismiss with actions */ fun willDismissWithAction(): Boolean { - return repository.onDismissAction.value?.onDismissAction != null + return bouncerView.delegate?.willDismissWithActions() == true } /** Returns whether the bouncer should be full screen. */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt index df260148751c..a22958b74bb9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt @@ -29,6 +29,7 @@ import com.android.keyguard.dagger.KeyguardBouncerComponent import com.android.systemui.keyguard.data.BouncerViewDelegate import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.collect @@ -75,6 +76,17 @@ object KeyguardBouncerViewBinder { hostViewController.showPrimarySecurityScreen() hostViewController.onResume() } + + override fun setDismissAction( + onDismissAction: ActivityStarter.OnDismissAction?, + cancelAction: Runnable? + ) { + hostViewController.setOnDismissAction(onDismissAction, cancelAction) + } + + override fun willDismissWithActions(): Boolean { + return hostViewController.hasDismissActions() + } } view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { @@ -122,15 +134,6 @@ object KeyguardBouncerViewBinder { } launch { - viewModel.setDismissAction.collect { - hostViewController.setOnDismissAction( - it.onDismissAction, - it.cancelAction - ) - } - } - - launch { viewModel.startDisappearAnimation.collect { hostViewController.startDisappearAnimation(it) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt index 9ad52117bfc6..9a9284371074 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt @@ -20,7 +20,6 @@ import android.view.View import com.android.systemui.keyguard.data.BouncerView import com.android.systemui.keyguard.data.BouncerViewDelegate import com.android.systemui.keyguard.domain.interactor.BouncerInteractor -import com.android.systemui.keyguard.shared.model.BouncerCallbackActionsModel import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE @@ -38,7 +37,7 @@ constructor( private val interactor: BouncerInteractor, ) { /** Observe on bouncer expansion amount. */ - val bouncerExpansionAmount: Flow<Float> = interactor.expansionAmount + val bouncerExpansionAmount: Flow<Float> = interactor.panelExpansionAmount /** Observe on bouncer visibility. */ val isBouncerVisible: Flow<Boolean> = interactor.isVisible @@ -63,9 +62,6 @@ constructor( /** Observe whether bouncer is starting to hide. */ val startingToHide: Flow<Unit> = interactor.startingToHide - /** Observe whether we want to set the dismiss action to the bouncer. */ - val setDismissAction: Flow<BouncerCallbackActionsModel> = interactor.onDismissAction - /** Observe whether we want to start the disappear animation. */ val startDisappearAnimation: Flow<Runnable> = interactor.startingDisappearAnimation diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index 592a156b9498..85d15dca12cb 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -85,7 +85,11 @@ import android.view.InsetsVisibilities; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.Surface; +import android.view.SurfaceControl; +import android.view.SurfaceControl.Transaction; import android.view.View; +import android.view.ViewRootImpl; +import android.view.ViewRootImpl.SurfaceChangedCallback; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.InternalInsetsInfo; import android.view.ViewTreeObserver.OnComputeInternalInsetsListener; @@ -354,15 +358,6 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } @Override - public void onQuickStepStarted() { - // Use navbar dragging as a signal to hide the rotate button - mView.getRotationButtonController().setRotateSuggestionButtonState(false); - - // Hide the notifications panel when quick step starts - mShadeController.collapsePanel(true /* animate */); - } - - @Override public void onPrioritizedRotation(@Surface.Rotation int rotation) { mStartingQuickSwitchRotation = rotation; if (rotation == -1) { @@ -475,6 +470,24 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } }; + private final ViewRootImpl.SurfaceChangedCallback mSurfaceChangedCallback = + new SurfaceChangedCallback() { + @Override + public void surfaceCreated(Transaction t) { + notifyNavigationBarSurface(); + } + + @Override + public void surfaceDestroyed() { + notifyNavigationBarSurface(); + } + + @Override + public void surfaceReplaced(Transaction t) { + notifyNavigationBarSurface(); + } + }; + @Inject NavigationBar( NavigationBarView navigationBarView, @@ -702,6 +715,8 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mView.getViewTreeObserver().addOnComputeInternalInsetsListener( mOnComputeInternalInsetsListener); + mView.getViewRootImpl().addSurfaceChangedCallback(mSurfaceChangedCallback); + notifyNavigationBarSurface(); mNavBarHelper.registerNavTaskStateUpdater(mNavbarTaskbarStateUpdater); @@ -780,6 +795,10 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mHandler.removeCallbacks(mEnableLayoutTransitions); mNavBarHelper.removeNavTaskStateUpdater(mNavbarTaskbarStateUpdater); mPipOptional.ifPresent(mView::removePipExclusionBoundsChangeListener); + ViewRootImpl viewRoot = mView.getViewRootImpl(); + if (viewRoot != null) { + viewRoot.removeSurfaceChangedCallback(mSurfaceChangedCallback); + } mFrame = null; mOrientationHandle = null; } @@ -933,6 +952,12 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } } + private void notifyNavigationBarSurface() { + ViewRootImpl viewRoot = mView.getViewRootImpl(); + SurfaceControl surface = viewRoot != null ? viewRoot.getSurfaceControl() : null; + mOverviewProxyService.onNavigationBarSurfaceChanged(surface); + } + private int deltaRotation(int oldRotation, int newRotation) { int delta = newRotation - oldRotation; if (delta < 0) delta += 4; diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java index 622f5a279a5f..83c2a5de5c6e 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java @@ -412,10 +412,6 @@ public class KeyButtonView extends ImageView implements ButtonInterface { logSomePresses(action, flags); if (mCode == KeyEvent.KEYCODE_BACK && flags != KeyEvent.FLAG_LONG_PRESS) { Log.i(TAG, "Back button event: " + KeyEvent.actionToString(action)); - if (action == MotionEvent.ACTION_UP) { - mOverviewProxyService.notifyBackAction((flags & KeyEvent.FLAG_CANCELED) == 0, - -1, -1, true /* isButton */, false /* gestureSwipeLeft */); - } } final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0; final KeyEvent ev = new KeyEvent(mDownTime, when, action, mCode, repeatCount, diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 709467ffd3b5..c319a827da44 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -287,8 +287,6 @@ public class EdgeBackGestureHandler extends CurrentUserTracker mBackAnimation.setTriggerBack(true); } - mOverviewProxyService.notifyBackAction(true, (int) mDownPoint.x, - (int) mDownPoint.y, false /* isButton */, !mIsOnLeftEdge); logGesture(mInRejectedExclusion ? SysUiStatsLog.BACK_GESTURE__TYPE__COMPLETED_REJECTED : SysUiStatsLog.BACK_GESTURE__TYPE__COMPLETED); @@ -300,8 +298,6 @@ public class EdgeBackGestureHandler extends CurrentUserTracker mBackAnimation.setTriggerBack(false); } logGesture(SysUiStatsLog.BACK_GESTURE__TYPE__INCOMPLETE); - mOverviewProxyService.notifyBackAction(false, (int) mDownPoint.x, - (int) mDownPoint.y, false /* isButton */, !mIsOnLeftEdge); } @Override @@ -785,9 +781,6 @@ public class EdgeBackGestureHandler extends CurrentUserTracker if (mExcludeRegion.contains(x, y)) { if (withinRange) { - // Log as exclusion only if it is in acceptable range in the first place. - mOverviewProxyService.notifyBackAction( - false /* completed */, -1, -1, false /* isButton */, !mIsOnLeftEdge); // We don't have the end point for logging purposes. mEndPoint.x = -1; mEndPoint.y = -1; diff --git a/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrier.java b/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrier.java index 703b95a082dc..b5ceeaed4904 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrier.java +++ b/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrier.java @@ -19,6 +19,7 @@ package com.android.systemui.qs.carrier; import android.annotation.StyleRes; import android.content.Context; import android.content.res.ColorStateList; +import android.content.res.Configuration; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; @@ -33,6 +34,7 @@ import com.android.settingslib.Utils; import com.android.settingslib.graph.SignalDrawable; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; +import com.android.systemui.util.LargeScreenUtils; import java.util.Objects; @@ -72,6 +74,7 @@ public class QSCarrier extends LinearLayout { mMobileSignal = findViewById(R.id.mobile_signal); mCarrierText = findViewById(R.id.qs_carrier_text); mSpacer = findViewById(R.id.spacer); + updateResources(); } /** @@ -142,4 +145,20 @@ public class QSCarrier extends LinearLayout { public void updateTextAppearance(@StyleRes int resId) { FontSizeUtils.updateFontSizeFromStyle(mCarrierText, resId); } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + updateResources(); + } + + private void updateResources() { + boolean useLargeScreenHeader = + LargeScreenUtils.shouldUseLargeScreenShadeHeader(getResources()); + mCarrierText.setMaxEms( + useLargeScreenHeader + ? Integer.MAX_VALUE + : getResources().getInteger(R.integer.qs_carrier_max_em) + ); + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java index e9a6c25c0e6d..1f92b12c1a83 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java @@ -140,7 +140,7 @@ public class QSIconViewImpl extends QSIconView { iv.setTag(R.id.qs_icon_tag, icon); iv.setTag(R.id.qs_slash_tag, state.slash); iv.setPadding(0, padding, 0, padding); - if (d instanceof Animatable2) { + if (shouldAnimate && d instanceof Animatable2) { Animatable2 a = (Animatable2) d; a.start(); if (state.isTransient) { diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 2bc343c1ca5e..46c4f410d078 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -64,6 +64,7 @@ import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.Surface; +import android.view.SurfaceControl; import android.view.accessibility.AccessibilityManager; import android.view.inputmethod.InputMethodManager; @@ -149,6 +150,7 @@ public class OverviewProxyService extends CurrentUserTracker implements private final UiEventLogger mUiEventLogger; private Region mActiveNavBarRegion; + private SurfaceControl mNavigationBarSurface; private IOverviewProxy mOverviewProxy; private int mConnectionBackoffAttempts; @@ -218,17 +220,15 @@ public class OverviewProxyService extends CurrentUserTracker implements } @Override - public void onBackPressed() throws RemoteException { + public void onBackPressed() { verifyCallerAndClearCallingIdentityPostMain("onBackPressed", () -> { sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK); sendEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK); - - notifyBackAction(true, -1, -1, true, false); }); } @Override - public void onImeSwitcherPressed() throws RemoteException { + public void onImeSwitcherPressed() { // TODO(b/204901476) We're intentionally using DEFAULT_DISPLAY for now since // Launcher/Taskbar isn't display aware. mContext.getSystemService(InputMethodManager.class) @@ -317,12 +317,6 @@ public class OverviewProxyService extends CurrentUserTracker implements } @Override - public void notifySwipeUpGestureStarted() { - verifyCallerAndClearCallingIdentityPostMain("notifySwipeUpGestureStarted", () -> - notifySwipeUpGestureStartedInternal()); - } - - @Override public void notifyPrioritizedRotation(@Surface.Rotation int rotation) { verifyCallerAndClearCallingIdentityPostMain("notifyPrioritizedRotation", () -> notifyPrioritizedRotationInternal(rotation)); @@ -444,6 +438,7 @@ public class OverviewProxyService extends CurrentUserTracker implements Log.e(TAG_OPS, "Failed to call onInitialize()", e); } dispatchNavButtonBounds(); + dispatchNavigationBarSurface(); // Force-update the systemui state flags updateSystemUiStateFlags(); @@ -598,11 +593,18 @@ public class OverviewProxyService extends CurrentUserTracker implements .commitUpdate(mContext.getDisplayId()); } - public void notifyBackAction(boolean completed, int downX, int downY, boolean isButton, - boolean gestureSwipeLeft) { + /** + * Called when the navigation bar surface is created or changed + */ + public void onNavigationBarSurfaceChanged(SurfaceControl navbarSurface) { + mNavigationBarSurface = navbarSurface; + dispatchNavigationBarSurface(); + } + + private void dispatchNavigationBarSurface() { try { if (mOverviewProxy != null) { - mOverviewProxy.onBackAction(completed, downX, downY, isButton, gestureSwipeLeft); + mOverviewProxy.onNavigationBarSurface(mNavigationBarSurface); } } catch (RemoteException e) { Log.e(TAG_OPS, "Failed to notify back action", e); @@ -801,24 +803,12 @@ public class OverviewProxyService extends CurrentUserTracker implements } } - public void notifyQuickStepStarted() { - for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { - mConnectionCallbacks.get(i).onQuickStepStarted(); - } - } - private void notifyPrioritizedRotationInternal(@Surface.Rotation int rotation) { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).onPrioritizedRotation(rotation); } } - public void notifyQuickScrubStarted() { - for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { - mConnectionCallbacks.get(i).onQuickScrubStarted(); - } - } - private void notifyAssistantProgress(@FloatRange(from = 0.0, to = 1.0) float progress) { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).onAssistantProgress(progress); @@ -837,12 +827,6 @@ public class OverviewProxyService extends CurrentUserTracker implements } } - private void notifySwipeUpGestureStartedInternal() { - for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { - mConnectionCallbacks.get(i).onSwipeUpGestureStarted(); - } - } - public void notifyAssistantVisibilityChanged(float visibility) { try { if (mOverviewProxy != null) { @@ -1006,23 +990,20 @@ public class OverviewProxyService extends CurrentUserTracker implements pw.print(" mWindowCornerRadius="); pw.println(mWindowCornerRadius); pw.print(" mSupportsRoundedCornersOnWindows="); pw.println(mSupportsRoundedCornersOnWindows); pw.print(" mActiveNavBarRegion="); pw.println(mActiveNavBarRegion); + pw.print(" mNavigationBarSurface="); pw.println(mNavigationBarSurface); pw.print(" mNavBarMode="); pw.println(mNavBarMode); mSysUiState.dump(pw, args); } public interface OverviewProxyListener { default void onConnectionChanged(boolean isConnected) {} - default void onQuickStepStarted() {} - default void onSwipeUpGestureStarted() {} default void onPrioritizedRotation(@Surface.Rotation int rotation) {} default void onOverviewShown(boolean fromHome) {} - default void onQuickScrubStarted() {} /** Notify the recents app (overview) is started by 3-button navigation. */ default void onToggleRecentApps() {} default void onHomeRotationEnabled(boolean enabled) {} default void onTaskbarStatusUpdated(boolean visible, boolean stashed) {} default void onTaskbarAutohideSuspend(boolean suspend) {} - default void onSystemUiStateChanged(int sysuiStateFlags) {} default void onAssistantProgress(@FloatRange(from = 0.0, to = 1.0) float progress) {} default void onAssistantGestureCompletion(float velocity) {} default void startAssistant(Bundle bundle) {} diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 31900ecd9478..fac2a97e65a3 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -4643,7 +4643,7 @@ public final class NotificationPanelViewController { mUpdateFlingVelocity = vel; } } else if (!mCentralSurfaces.isBouncerShowing() - && !mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating() + && !mStatusBarKeyguardViewManager.isShowingAlternateAuth() && !mKeyguardStateController.isKeyguardGoingAway()) { onEmptySpaceClick(); onTrackingStopped(true); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 65bd58d0d801..1e63b2dd134f 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -284,7 +284,7 @@ public class NotificationShadeWindowViewController { return true; } - if (mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()) { + if (mStatusBarKeyguardViewManager.isShowingAlternateAuth()) { // capture all touches if the alt auth bouncer is showing return true; } @@ -322,7 +322,7 @@ public class NotificationShadeWindowViewController { handled = !mService.isPulsing(); } - if (mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()) { + if (mStatusBarKeyguardViewManager.isShowingAlternateAuth()) { // eat the touch handled = true; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 87ef92a28d5d..408293cffd99 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -496,9 +496,6 @@ public class NotificationShelf extends ActivatableNotificationView implements return; } - final float smallCornerRadius = - getResources().getDimension(R.dimen.notification_corner_radius_small) - / getResources().getDimension(R.dimen.notification_corner_radius); final float viewEnd = viewStart + anv.getActualHeight(); final float cornerAnimationDistance = mCornerAnimationDistance * mAmbientState.getExpansionFraction(); @@ -509,7 +506,7 @@ public class NotificationShelf extends ActivatableNotificationView implements final float changeFraction = MathUtils.saturate( (viewEnd - cornerAnimationTop) / cornerAnimationDistance); anv.requestBottomRoundness( - anv.isLastInSection() ? 1f : changeFraction, + /* value = */ anv.isLastInSection() ? 1f : changeFraction, /* animate = */ false, SourceType.OnScroll); @@ -517,7 +514,7 @@ public class NotificationShelf extends ActivatableNotificationView implements // Fast scroll skips frames and leaves corners with unfinished rounding. // Reset top and bottom corners outside of animation bounds. anv.requestBottomRoundness( - anv.isLastInSection() ? 1f : smallCornerRadius, + /* value = */ anv.isLastInSection() ? 1f : 0f, /* animate = */ false, SourceType.OnScroll); } @@ -527,16 +524,16 @@ public class NotificationShelf extends ActivatableNotificationView implements final float changeFraction = MathUtils.saturate( (viewStart - cornerAnimationTop) / cornerAnimationDistance); anv.requestTopRoundness( - anv.isFirstInSection() ? 1f : changeFraction, - false, + /* value = */ anv.isFirstInSection() ? 1f : changeFraction, + /* animate = */ false, SourceType.OnScroll); } else if (viewStart < cornerAnimationTop) { // Fast scroll skips frames and leaves corners with unfinished rounding. // Reset top and bottom corners outside of animation bounds. anv.requestTopRoundness( - anv.isFirstInSection() ? 1f : smallCornerRadius, - false, + /* value = */ anv.isFirstInSection() ? 1f : 0f, + /* animate = */ false, SourceType.OnScroll); } } @@ -976,6 +973,16 @@ public class NotificationShelf extends ActivatableNotificationView implements mIndexOfFirstViewInShelf = mHostLayoutController.indexOfChild(firstViewInShelf); } + /** + * This method resets the OnScroll roundness of a view to 0f + * + * Note: This should be the only class that handles roundness {@code SourceType.OnScroll} + */ + public static void resetOnScrollRoundness(ExpandableView expandableView) { + expandableView.requestTopRoundness(0f, false, SourceType.OnScroll); + expandableView.requestBottomRoundness(0f, false, SourceType.OnScroll); + } + public class ShelfState extends ExpandableViewState { private boolean hasItemsInStableShelf; private ExpandableView firstViewInShelf; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index 26f0ad9eca87..0554fb5b3689 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -44,6 +44,7 @@ import com.android.internal.widget.NotificationExpandButton; import com.android.systemui.R; import com.android.systemui.statusbar.CrossFadeHelper; import com.android.systemui.statusbar.NotificationGroupingUtil; +import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.notification.FeedbackIcon; import com.android.systemui.statusbar.notification.NotificationFadeAware; import com.android.systemui.statusbar.notification.NotificationUtils; @@ -308,6 +309,11 @@ public class NotificationChildrenContainer extends ViewGroup row.setContentTransformationAmount(0, false /* isLastChild */); row.setNotificationFaded(mContainingNotificationIsFaded); + + // This is a workaround, the NotificationShelf should be the owner of `OnScroll` roundness. + // Here we should reset the `OnScroll` roundness only on top-level rows. + NotificationShelf.resetOnScrollRoundness(row); + // It doesn't make sense to keep old animations around, lets cancel them! ExpandableViewState viewState = row.getViewState(); if (viewState != null) { @@ -1377,8 +1383,12 @@ public class NotificationChildrenContainer extends ViewGroup if (child.getVisibility() == View.GONE) { continue; } + child.requestTopRoundness( + /* value = */ 0f, + /* animate = */ isShown(), + SourceType.DefaultValue); child.requestBottomRoundness( - last ? getBottomRoundness() : 0f, + /* value = */ last ? getBottomRoundness() : 0f, /* animate = */ isShown(), SourceType.DefaultValue); last = false; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java index 9bb4132490d4..b2a9509a03b7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java @@ -64,6 +64,11 @@ public class KeyguardBouncer { private static final String TAG = "KeyguardBouncer"; static final long BOUNCER_FACE_DELAY = 1200; public static final float ALPHA_EXPANSION_THRESHOLD = 0.95f; + /** + * Values for the bouncer expansion represented as the panel expansion. + * Panel expansion 1f = panel fully showing = bouncer fully hidden + * Panel expansion 0f = panel fully hiding = bouncer fully showing + */ public static final float EXPANSION_HIDDEN = 1f; public static final float EXPANSION_VISIBLE = 0f; @@ -143,6 +148,14 @@ public class KeyguardBouncer { } /** + * Get the KeyguardBouncer expansion + * @return 1=HIDDEN, 0=SHOWING, in between 0 and 1 means the bouncer is in transition. + */ + public float getExpansion() { + return mExpansion; + } + + /** * Enable/disable only the back button */ public void setBackButtonEnabled(boolean enabled) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 318907da89f8..a00e75642f55 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -86,8 +86,10 @@ import com.android.systemui.unfold.SysUIUnfoldComponent; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.HashSet; import java.util.Objects; import java.util.Optional; +import java.util.Set; import javax.inject.Inject; @@ -166,13 +168,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb @Override public void onExpansionChanged(float expansion) { - if (mAlternateAuthInterceptor != null) { - mAlternateAuthInterceptor.setBouncerExpansionChanged(expansion); - } if (mBouncerAnimating) { mCentralSurfaces.setBouncerHiddenFraction(expansion); } - updateStates(); } @Override @@ -184,9 +182,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb if (!isVisible) { mCentralSurfaces.setBouncerHiddenFraction(KeyguardBouncer.EXPANSION_HIDDEN); } - if (mAlternateAuthInterceptor != null) { - mAlternateAuthInterceptor.onBouncerVisibilityChanged(); - } /* Register predictive back callback when keyguard becomes visible, and unregister when it's hidden. */ @@ -252,6 +247,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private int mLastBiometricMode; private boolean mLastScreenOffAnimationPlaying; private float mQsExpansion; + final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>(); private boolean mIsModernBouncerEnabled; private OnDismissAction mAfterKeyguardGoneAction; @@ -465,7 +461,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb if (mBouncer != null) { mBouncer.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN); } else { - mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN); + mBouncerInteractor.setPanelExpansion(KeyguardBouncer.EXPANSION_HIDDEN); } } else if (mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED) { // Don't expand to the bouncer. Instead transition back to the lock screen (see @@ -475,7 +471,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb if (mBouncer != null) { mBouncer.setExpansion(KeyguardBouncer.EXPANSION_VISIBLE); } else { - mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_VISIBLE); + mBouncerInteractor.setPanelExpansion(KeyguardBouncer.EXPANSION_VISIBLE); } } else if (mKeyguardStateController.isShowing() && !hideBouncerOverDream) { if (!isWakeAndUnlocking() @@ -485,7 +481,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb if (mBouncer != null) { mBouncer.setExpansion(fraction); } else { - mBouncerInteractor.setExpansion(fraction); + mBouncerInteractor.setPanelExpansion(fraction); } } if (fraction != KeyguardBouncer.EXPANSION_HIDDEN && tracking @@ -504,7 +500,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb if (mBouncer != null) { mBouncer.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN); } else { - mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN); + mBouncerInteractor.setPanelExpansion(KeyguardBouncer.EXPANSION_HIDDEN); } } else if (mPulsing && fraction == KeyguardBouncer.EXPANSION_VISIBLE) { // Panel expanded while pulsing but didn't translate the bouncer (because we are @@ -1356,7 +1352,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mBouncerInteractor.notifyKeyguardAuthenticated(strongAuth); } - if (mAlternateAuthInterceptor != null && isShowingAlternateAuthOrAnimating()) { + if (mAlternateAuthInterceptor != null && isShowingAlternateAuth()) { resetAlternateAuth(false); executeAfterKeyguardGoneAction(); } @@ -1442,6 +1438,10 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb pw.println(" mPendingWakeupAction: " + mPendingWakeupAction); pw.println(" isBouncerShowing(): " + isBouncerShowing()); pw.println(" bouncerIsOrWillBeShowing(): " + bouncerIsOrWillBeShowing()); + pw.println(" Registered KeyguardViewManagerCallbacks:"); + for (KeyguardViewManagerCallback callback : mCallbacks) { + pw.println(" " + callback); + } if (mBouncer != null) { mBouncer.dump(pw); @@ -1466,6 +1466,20 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } /** + * Add a callback to listen for changes + */ + public void addCallback(KeyguardViewManagerCallback callback) { + mCallbacks.add(callback); + } + + /** + * Removes callback to stop receiving updates + */ + public void removeCallback(KeyguardViewManagerCallback callback) { + mCallbacks.remove(callback); + } + + /** * Whether qs is currently expanded. */ public float getQsExpansion() { @@ -1477,8 +1491,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb */ public void setQsExpansion(float qsExpansion) { mQsExpansion = qsExpansion; - if (mAlternateAuthInterceptor != null) { - mAlternateAuthInterceptor.setQsExpansion(qsExpansion); + for (KeyguardViewManagerCallback callback : mCallbacks) { + callback.onQSExpansionChanged(mQsExpansion); } } @@ -1492,21 +1506,13 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb && mAlternateAuthInterceptor.isShowingAlternateAuthBouncer(); } - public boolean isShowingAlternateAuthOrAnimating() { - return mAlternateAuthInterceptor != null - && (mAlternateAuthInterceptor.isShowingAlternateAuthBouncer() - || mAlternateAuthInterceptor.isAnimating()); - } - /** - * Forward touches to any alternate authentication affordances. + * Forward touches to callbacks. */ - public boolean onTouch(MotionEvent event) { - if (mAlternateAuthInterceptor == null) { - return false; + public void onTouch(MotionEvent event) { + for (KeyguardViewManagerCallback callback: mCallbacks) { + callback.onTouch(event); } - - return mAlternateAuthInterceptor.onTouch(event); } /** Update keyguard position based on a tapped X coordinate. */ @@ -1640,45 +1646,33 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb boolean isShowingAlternateAuthBouncer(); /** - * print information for the alternate auth interceptor registered - */ - void dump(PrintWriter pw); - - /** - * @return true if the new auth method bouncer is currently animating in or out. - */ - boolean isAnimating(); - - /** - * How much QS is fully expanded where 0f is not showing and 1f is fully expanded. - */ - void setQsExpansion(float qsExpansion); - - /** - * Forward potential touches to authentication interceptor - * @return true if event was handled + * Use when an app occluding the keyguard would like to give the user ability to + * unlock the device using udfps. + * + * @param color of the udfps icon. should have proper contrast with its background. only + * used if requestUdfps = true */ - boolean onTouch(MotionEvent event); + void requestUdfps(boolean requestUdfps, int color); /** - * Update pin/pattern/password bouncer expansion amount where 0 is visible and 1 is fully - * hidden + * print information for the alternate auth interceptor registered */ - void setBouncerExpansionChanged(float expansion); + void dump(PrintWriter pw); + } + /** + * Callback for KeyguardViewManager state changes. + */ + public interface KeyguardViewManagerCallback { /** - * called when the bouncer view visibility has changed. + * Set the amount qs is expanded. For example, swipe down from the top of the + * lock screen to start the full QS expansion. */ - void onBouncerVisibilityChanged(); + default void onQSExpansionChanged(float qsExpansion) { } /** - * Use when an app occluding the keyguard would like to give the user ability to - * unlock the device using udfps. - * - * @param color of the udfps icon. should have proper contrast with its background. only - * used if requestUdfps = true + * Forward touch events to callbacks */ - void requestUdfps(boolean requestUdfps, int color); - + default void onTouch(MotionEvent event) { } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt new file mode 100644 index 000000000000..e61890523ebb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt @@ -0,0 +1,27 @@ +/* + * 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.statusbar.pipeline.mobile.data.model + +import android.net.NetworkCapabilities + +/** Provides information about a mobile network connection */ +data class MobileConnectivityModel( + /** Whether mobile is the connected transport see [NetworkCapabilities.TRANSPORT_CELLULAR] */ + val isConnected: Boolean = false, + /** Whether the mobile transport is validated [NetworkCapabilities.NET_CAPABILITY_VALIDATED] */ + val isValidated: Boolean = false, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt index 06e8f467ee0b..581842bc2f57 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt @@ -16,11 +16,15 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository +import android.content.Context +import android.database.ContentObserver +import android.provider.Settings.Global import android.telephony.CellSignalStrength import android.telephony.CellSignalStrengthCdma import android.telephony.ServiceState import android.telephony.SignalStrength import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager import android.telephony.TelephonyCallback import android.telephony.TelephonyDisplayInfo import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE @@ -34,6 +38,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetwork import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange +import com.android.systemui.util.settings.GlobalSettings import java.lang.IllegalStateException import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -42,9 +47,12 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn /** @@ -65,14 +73,23 @@ interface MobileConnectionRepository { */ val subscriptionModelFlow: Flow<MobileSubscriptionModel> /** Observable tracking [TelephonyManager.isDataConnectionAllowed] */ - val dataEnabled: Flow<Boolean> + val dataEnabled: StateFlow<Boolean> + /** + * True if this connection represents the default subscription per + * [SubscriptionManager.getDefaultDataSubscriptionId] + */ + val isDefaultDataSubscription: StateFlow<Boolean> } @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) class MobileConnectionRepositoryImpl( + private val context: Context, private val subId: Int, private val telephonyManager: TelephonyManager, + private val globalSettings: GlobalSettings, + defaultDataSubId: StateFlow<Int>, + globalMobileDataSettingChangedEvent: Flow<Unit>, bgDispatcher: CoroutineDispatcher, logger: ConnectivityPipelineLogger, scope: CoroutineScope, @@ -86,6 +103,8 @@ class MobileConnectionRepositoryImpl( } } + private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1) + override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = run { var state = MobileSubscriptionModel() conflatedCallbackFlow { @@ -165,33 +184,75 @@ class MobileConnectionRepositoryImpl( telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } } + .onEach { telephonyCallbackEvent.tryEmit(Unit) } .logOutputChange(logger, "MobileSubscriptionModel") .stateIn(scope, SharingStarted.WhileSubscribed(), state) } + /** Produces whenever the mobile data setting changes for this subId */ + private val localMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + + globalSettings.registerContentObserver( + globalSettings.getUriFor("${Global.MOBILE_DATA}$subId"), + /* notifyForDescendants */ true, + observer + ) + + awaitClose { context.contentResolver.unregisterContentObserver(observer) } + } + /** * There are a few cases where we will need to poll [TelephonyManager] so we can update some * internal state where callbacks aren't provided. Any of those events should be merged into * this flow, which can be used to trigger the polling. */ - private val telephonyPollingEvent: Flow<Unit> = subscriptionModelFlow.map {} + private val telephonyPollingEvent: Flow<Unit> = + merge( + telephonyCallbackEvent, + localMobileDataSettingChangedEvent, + globalMobileDataSettingChangedEvent, + ) - override val dataEnabled: Flow<Boolean> = telephonyPollingEvent.map { dataConnectionAllowed() } + override val dataEnabled: StateFlow<Boolean> = + telephonyPollingEvent + .mapLatest { dataConnectionAllowed() } + .stateIn(scope, SharingStarted.WhileSubscribed(), dataConnectionAllowed()) private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed + override val isDefaultDataSubscription: StateFlow<Boolean> = + defaultDataSubId + .mapLatest { it == subId } + .stateIn(scope, SharingStarted.WhileSubscribed(), defaultDataSubId.value == subId) + class Factory @Inject constructor( + private val context: Context, private val telephonyManager: TelephonyManager, private val logger: ConnectivityPipelineLogger, + private val globalSettings: GlobalSettings, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, ) { - fun build(subId: Int): MobileConnectionRepository { + fun build( + subId: Int, + defaultDataSubId: StateFlow<Int>, + globalMobileDataSettingChangedEvent: Flow<Unit>, + ): MobileConnectionRepository { return MobileConnectionRepositoryImpl( + context, subId, telephonyManager.createForSubscriptionId(subId), + globalSettings, + defaultDataSubId, + globalMobileDataSettingChangedEvent, bgDispatcher, logger, scope, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt index 0e2428ae393a..c3c1f1403c60 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt @@ -16,15 +16,27 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository +import android.annotation.SuppressLint import android.content.Context import android.content.IntentFilter +import android.database.ContentObserver +import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED +import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +import android.provider.Settings +import android.provider.Settings.Global.MOBILE_DATA import android.telephony.CarrierConfigManager import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import android.telephony.TelephonyCallback import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener import android.telephony.TelephonyManager import androidx.annotation.VisibleForTesting +import com.android.internal.telephony.PhoneConstants import com.android.settingslib.mobile.MobileMappings import com.android.settingslib.mobile.MobileMappings.Config import com.android.systemui.broadcast.BroadcastDispatcher @@ -32,7 +44,9 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.util.settings.GlobalSettings import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -40,10 +54,12 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext @@ -57,13 +73,22 @@ interface MobileConnectionsRepository { val subscriptionsFlow: Flow<List<SubscriptionInfo>> /** Observable for the subscriptionId of the current mobile data connection */ - val activeMobileDataSubscriptionId: Flow<Int> + val activeMobileDataSubscriptionId: StateFlow<Int> /** Observable for [MobileMappings.Config] tracking the defaults */ val defaultDataSubRatConfig: StateFlow<Config> + /** Tracks [SubscriptionManager.getDefaultDataSubscriptionId] */ + val defaultDataSubId: StateFlow<Int> + + /** The current connectivity status for the default mobile network connection */ + val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> + /** Get or create a repository for the line of service for the given subscription ID */ fun getRepoForSubId(subId: Int): MobileConnectionRepository + + /** Observe changes to the [Settings.Global.MOBILE_DATA] setting */ + val globalMobileDataSettingChangedEvent: Flow<Unit> } @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @@ -72,10 +97,12 @@ interface MobileConnectionsRepository { class MobileConnectionsRepositoryImpl @Inject constructor( + private val connectivityManager: ConnectivityManager, private val subscriptionManager: SubscriptionManager, private val telephonyManager: TelephonyManager, private val logger: ConnectivityPipelineLogger, broadcastDispatcher: BroadcastDispatcher, + private val globalSettings: GlobalSettings, private val context: Context, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, @@ -121,17 +148,26 @@ constructor( telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } } + .stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID) + + private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> = + MutableSharedFlow(extraBufferCapacity = 1) + + override val defaultDataSubId: StateFlow<Int> = + broadcastDispatcher + .broadcastFlow( + IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) + ) { intent, _ -> + intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID) + } + .distinctUntilChanged() + .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) } .stateIn( scope, - started = SharingStarted.WhileSubscribed(), - SubscriptionManager.INVALID_SUBSCRIPTION_ID + SharingStarted.WhileSubscribed(), + SubscriptionManager.getDefaultDataSubscriptionId() ) - private val defaultDataSubChangedEvent = - broadcastDispatcher.broadcastFlow( - IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) - ) - private val carrierConfigChangedEvent = broadcastDispatcher.broadcastFlow( IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED) @@ -148,9 +184,8 @@ constructor( * This flow will produce whenever the default data subscription or the carrier config changes. */ override val defaultDataSubRatConfig: StateFlow<Config> = - combine(defaultDataSubChangedEvent, carrierConfigChangedEvent) { _, _ -> - Config.readConfig(context) - } + merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent) + .mapLatest { Config.readConfig(context) } .stateIn( scope, SharingStarted.WhileSubscribed(), @@ -168,6 +203,57 @@ constructor( ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it } } + /** + * In single-SIM devices, the [MOBILE_DATA] setting is phone-wide. For multi-SIM, the individual + * connection repositories also observe the URI for [MOBILE_DATA] + subId. + */ + override val globalMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + + globalSettings.registerContentObserver( + globalSettings.getUriFor(MOBILE_DATA), + true, + observer + ) + + awaitClose { context.contentResolver.unregisterContentObserver(observer) } + } + + @SuppressLint("MissingPermission") + override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> = + conflatedCallbackFlow { + val callback = + object : NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { + override fun onLost(network: Network) { + // Send a disconnected model when lost. Maybe should create a sealed + // type or null here? + trySend(MobileConnectivityModel()) + } + + override fun onCapabilitiesChanged( + network: Network, + caps: NetworkCapabilities + ) { + trySend( + MobileConnectivityModel( + isConnected = caps.hasTransport(TRANSPORT_CELLULAR), + isValidated = caps.hasCapability(NET_CAPABILITY_VALIDATED), + ) + ) + } + } + + connectivityManager.registerDefaultNetworkCallback(callback) + + awaitClose { connectivityManager.unregisterNetworkCallback(callback) } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel()) + private fun isValidSubId(subId: Int): Boolean { subscriptionsFlow.value.forEach { if (it.subscriptionId == subId) { @@ -181,7 +267,11 @@ constructor( @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository { - return mobileConnectionRepositoryFactory.build(subId) + return mobileConnectionRepositoryFactory.build( + subId, + defaultDataSubId, + globalMobileDataSettingChangedEvent, + ) } private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt index 77de849691db..91886bb121d5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt @@ -26,7 +26,6 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.mapLatest @@ -40,7 +39,7 @@ import kotlinx.coroutines.withContext */ interface UserSetupRepository { /** Observable tracking [DeviceProvisionedController.isUserSetup] */ - val isUserSetupFlow: Flow<Boolean> + val isUserSetupFlow: StateFlow<Boolean> } @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index f99d278c3903..0da84f0bec9c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -18,81 +18,109 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.CarrierConfigManager import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.util.CarrierConfigTracker -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn interface MobileIconInteractor { + /** Only true if mobile is the default transport but is not validated, otherwise false */ + val isDefaultConnectionFailed: StateFlow<Boolean> + + /** True when telephony tells us that the data state is CONNECTED */ + val isDataConnected: StateFlow<Boolean> + + // TODO(b/256839546): clarify naming of default vs active + /** True if we want to consider the data connection enabled */ + val isDefaultDataEnabled: StateFlow<Boolean> + /** Observable for the data enabled state of this connection */ - val isDataEnabled: Flow<Boolean> + val isDataEnabled: StateFlow<Boolean> /** Observable for RAT type (network type) indicator */ - val networkTypeIconGroup: Flow<MobileIconGroup> + val networkTypeIconGroup: StateFlow<MobileIconGroup> /** True if this line of service is emergency-only */ - val isEmergencyOnly: Flow<Boolean> + val isEmergencyOnly: StateFlow<Boolean> /** Int describing the connection strength. 0-4 OR 1-5. See [numberOfLevels] */ - val level: Flow<Int> + val level: StateFlow<Int> /** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */ - val numberOfLevels: Flow<Int> - - /** True when we want to draw an icon that makes room for the exclamation mark */ - val cutOut: Flow<Boolean> + val numberOfLevels: StateFlow<Int> } /** Interactor for a single mobile connection. This connection _should_ have one subscription ID */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) class MobileIconInteractorImpl( - defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>, - defaultMobileIconGroup: Flow<MobileIconGroup>, + @Application scope: CoroutineScope, + defaultSubscriptionHasDataEnabled: StateFlow<Boolean>, + defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>, + defaultMobileIconGroup: StateFlow<MobileIconGroup>, + override val isDefaultConnectionFailed: StateFlow<Boolean>, mobileMappingsProxy: MobileMappingsProxy, connectionRepository: MobileConnectionRepository, ) : MobileIconInteractor { private val mobileStatusInfo = connectionRepository.subscriptionModelFlow - override val isDataEnabled: Flow<Boolean> = connectionRepository.dataEnabled + override val isDataEnabled: StateFlow<Boolean> = connectionRepository.dataEnabled + + override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled /** Observable for the current RAT indicator icon ([MobileIconGroup]) */ - override val networkTypeIconGroup: Flow<MobileIconGroup> = + override val networkTypeIconGroup: StateFlow<MobileIconGroup> = combine( - mobileStatusInfo, - defaultMobileIconMapping, - defaultMobileIconGroup, - ) { info, mapping, defaultGroup -> - val lookupKey = - when (val resolved = info.resolvedNetworkType) { - is DefaultNetworkType -> mobileMappingsProxy.toIconKey(resolved.type) - is OverrideNetworkType -> mobileMappingsProxy.toIconKeyOverride(resolved.type) + mobileStatusInfo, + defaultMobileIconMapping, + defaultMobileIconGroup, + ) { info, mapping, defaultGroup -> + val lookupKey = + when (val resolved = info.resolvedNetworkType) { + is DefaultNetworkType -> mobileMappingsProxy.toIconKey(resolved.type) + is OverrideNetworkType -> + mobileMappingsProxy.toIconKeyOverride(resolved.type) + } + mapping[lookupKey] ?: defaultGroup + } + .stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value) + + override val isEmergencyOnly: StateFlow<Boolean> = + mobileStatusInfo + .mapLatest { it.isEmergencyOnly } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val level: StateFlow<Int> = + mobileStatusInfo + .mapLatest { mobileModel -> + // TODO: incorporate [MobileMappings.Config.alwaysShowCdmaRssi] + if (mobileModel.isGsm) { + mobileModel.primaryLevel + } else { + mobileModel.cdmaLevel } - mapping[lookupKey] ?: defaultGroup - } - - override val isEmergencyOnly: Flow<Boolean> = mobileStatusInfo.map { it.isEmergencyOnly } - - override val level: Flow<Int> = - mobileStatusInfo.map { mobileModel -> - // TODO: incorporate [MobileMappings.Config.alwaysShowCdmaRssi] - if (mobileModel.isGsm) { - mobileModel.primaryLevel - } else { - mobileModel.cdmaLevel } - } + .stateIn(scope, SharingStarted.WhileSubscribed(), 0) /** * This will become variable based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL] * once it's wired up inside of [CarrierConfigTracker] */ - override val numberOfLevels: Flow<Int> = flowOf(4) + override val numberOfLevels: StateFlow<Int> = MutableStateFlow(4) - /** Whether or not to draw the mobile triangle as "cut out", i.e., with the exclamation mark */ - // TODO: find a better name for this? - override val cutOut: Flow<Boolean> = flowOf(false) + override val isDataConnected: StateFlow<Boolean> = + mobileStatusInfo + .mapLatest { subscriptionModel -> subscriptionModel.dataConnectionState == Connected } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt index 614d583c3c48..a4175c3a6ab1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.CarrierConfigManager import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.dagger.SysUISingleton @@ -35,7 +36,9 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn /** @@ -51,12 +54,16 @@ import kotlinx.coroutines.flow.stateIn interface MobileIconsInteractor { /** List of subscriptions, potentially filtered for CBRS */ val filteredSubscriptions: Flow<List<SubscriptionInfo>> + /** True if the active mobile data subscription has data enabled */ + val activeDataConnectionHasDataEnabled: StateFlow<Boolean> /** The icon mapping from network type to [MobileIconGroup] for the default subscription */ - val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>> + val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */ - val defaultMobileIconGroup: Flow<MobileIconGroup> + val defaultMobileIconGroup: StateFlow<MobileIconGroup> + /** True only if the default network is mobile, and validation also failed */ + val isDefaultConnectionFailed: StateFlow<Boolean> /** True once the user has been set up */ - val isUserSetup: Flow<Boolean> + val isUserSetup: StateFlow<Boolean> /** * Vends out a [MobileIconInteractor] tracking the [MobileConnectionRepository] for the given * subId. Will throw if the ID is invalid @@ -79,6 +86,22 @@ constructor( private val activeMobileDataSubscriptionId = mobileConnectionsRepo.activeMobileDataSubscriptionId + private val activeMobileDataConnectionRepo: StateFlow<MobileConnectionRepository?> = + activeMobileDataSubscriptionId + .mapLatest { activeId -> + if (activeId == INVALID_SUBSCRIPTION_ID) { + null + } else { + mobileConnectionsRepo.getRepoForSubId(activeId) + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), null) + + override val activeDataConnectionHasDataEnabled: StateFlow<Boolean> = + activeMobileDataConnectionRepo + .flatMapLatest { it?.dataEnabled ?: flowOf(false) } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + private val unfilteredSubscriptions: Flow<List<SubscriptionInfo>> = mobileConnectionsRepo.subscriptionsFlow @@ -132,22 +155,40 @@ constructor( */ override val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> = mobileConnectionsRepo.defaultDataSubRatConfig - .map { mobileMappingsProxy.mapIconSets(it) } + .mapLatest { mobileMappingsProxy.mapIconSets(it) } .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = mapOf()) /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */ override val defaultMobileIconGroup: StateFlow<MobileIconGroup> = mobileConnectionsRepo.defaultDataSubRatConfig - .map { mobileMappingsProxy.getDefaultIcons(it) } + .mapLatest { mobileMappingsProxy.getDefaultIcons(it) } .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = TelephonyIcons.G) - override val isUserSetup: Flow<Boolean> = userSetupRepo.isUserSetupFlow + /** + * We want to show an error state when cellular has actually failed to validate, but not if some + * other transport type is active, because then we expect there not to be validation. + */ + override val isDefaultConnectionFailed: StateFlow<Boolean> = + mobileConnectionsRepo.defaultMobileNetworkConnectivity + .mapLatest { connectivityModel -> + if (!connectivityModel.isConnected) { + false + } else { + !connectivityModel.isValidated + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val isUserSetup: StateFlow<Boolean> = userSetupRepo.isUserSetupFlow /** Vends out new [MobileIconInteractor] for a particular subId */ override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor = MobileIconInteractorImpl( + scope, + activeDataConnectionHasDataEnabled, defaultMobileIconMapping, defaultMobileIconGroup, + isDefaultConnectionFailed, mobileMappingsProxy, mobileConnectionsRepo.getRepoForSubId(subId), ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index 81317398f086..7869021c0501 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -24,10 +24,12 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIc import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.mapLatest /** * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over @@ -39,29 +41,38 @@ import kotlinx.coroutines.flow.flowOf * * TODO: figure out where carrier merged and VCN models go (probably here?) */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) class MobileIconViewModel constructor( val subscriptionId: Int, iconInteractor: MobileIconInteractor, logger: ConnectivityPipelineLogger, ) { + /** Whether or not to show the error state of [SignalDrawable] */ + private val showExclamationMark: Flow<Boolean> = + iconInteractor.isDefaultDataEnabled.mapLatest { !it } + /** An int consumable by [SignalDrawable] for display */ - var iconId: Flow<Int> = - combine(iconInteractor.level, iconInteractor.numberOfLevels, iconInteractor.cutOut) { + val iconId: Flow<Int> = + combine(iconInteractor.level, iconInteractor.numberOfLevels, showExclamationMark) { level, numberOfLevels, - cutOut -> - SignalDrawable.getState(level, numberOfLevels, cutOut) + showExclamationMark -> + SignalDrawable.getState(level, numberOfLevels, showExclamationMark) } .distinctUntilChanged() .logOutputChange(logger, "iconId($subscriptionId)") /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ - var networkTypeIcon: Flow<Icon?> = - combine(iconInteractor.networkTypeIconGroup, iconInteractor.isDataEnabled) { - networkTypeIconGroup, - isDataEnabled -> - if (!isDataEnabled) { + val networkTypeIcon: Flow<Icon?> = + combine( + iconInteractor.networkTypeIconGroup, + iconInteractor.isDataConnected, + iconInteractor.isDataEnabled, + iconInteractor.isDefaultConnectionFailed, + ) { networkTypeIconGroup, dataConnected, dataEnabled, failedConnection -> + if (!dataConnected || !dataEnabled || failedConnection) { null } else { val desc = @@ -72,5 +83,5 @@ constructor( } } - var tint: Flow<Int> = flowOf(Color.CYAN) + val tint: Flow<Int> = flowOf(Color.CYAN) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthCredentialViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthCredentialViewTest.kt new file mode 100644 index 000000000000..fbda08fab799 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthCredentialViewTest.kt @@ -0,0 +1,163 @@ +/* + * 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.biometrics + +import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING +import android.hardware.biometrics.BiometricManager +import android.hardware.biometrics.PromptInfo +import android.hardware.face.FaceSensorPropertiesInternal +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal +import android.os.Handler +import android.os.IBinder +import android.os.UserManager +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.ViewUtils +import androidx.test.filters.SmallTest +import com.android.internal.jank.InteractionJankMonitor +import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockPatternView +import com.android.internal.widget.VerifyCredentialResponse +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.AuthContainerView.BiometricCallback +import com.android.systemui.biometrics.AuthCredentialView.ErrorTimer +import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnit + +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@SmallTest +class AuthCredentialViewTest : SysuiTestCase() { + @JvmField @Rule var mockitoRule = MockitoJUnit.rule() + + @Mock lateinit var callback: AuthDialogCallback + @Mock lateinit var lockPatternUtils: LockPatternUtils + @Mock lateinit var userManager: UserManager + @Mock lateinit var wakefulnessLifecycle: WakefulnessLifecycle + @Mock lateinit var windowToken: IBinder + @Mock lateinit var interactionJankMonitor: InteractionJankMonitor + + private var authContainer: TestAuthContainerView? = null + private var authCredentialView: AuthCredentialPatternView? = null + private var lockPatternView: LockPatternView? = null + private var biometricCallback: BiometricCallback? = null + private var errorTimer: ErrorTimer? = null + + @After + fun tearDown() { + if (authContainer?.isAttachedToWindow == true) { + ViewUtils.detachView(authContainer) + } + } + + @Test + fun testAuthCredentialPatternView_onErrorTimeoutFinish_setPatternEnabled() { + `when`(lockPatternUtils.getCredentialTypeForUser(anyInt())) + .thenReturn(LockPatternUtils.CREDENTIAL_TYPE_PATTERN) + `when`(lockPatternUtils.getKeyguardStoredPasswordQuality(anyInt())) + .thenReturn(PASSWORD_QUALITY_SOMETHING) + val errorResponse: VerifyCredentialResponse = VerifyCredentialResponse.fromError() + + assertThat(initializeFingerprintContainer()).isNotNull() + authContainer?.animateToCredentialUI() + waitForIdleSync() + + authCredentialView = spy(authContainer?.mCredentialView as AuthCredentialPatternView) + authCredentialView?.onCredentialVerified(errorResponse, 5000) + errorTimer = authCredentialView?.mErrorTimer + errorTimer?.onFinish() + waitForIdleSync() + + verify(authCredentialView)?.onErrorTimeoutFinish() + + lockPatternView = authCredentialView?.mLockPatternView + assertThat(lockPatternView?.isEnabled).isTrue() + } + + private fun initializeFingerprintContainer( + authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK, + addToView: Boolean = true + ) = + initializeContainer( + TestAuthContainerView( + authenticators = authenticators, + fingerprintProps = fingerprintSensorPropertiesInternal() + ), + addToView + ) + + private fun initializeContainer( + view: TestAuthContainerView, + addToView: Boolean + ): TestAuthContainerView { + authContainer = view + if (addToView) { + authContainer!!.addToView() + biometricCallback = authContainer?.mBiometricCallback + } + return authContainer!! + } + + private inner class TestAuthContainerView( + authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK, + fingerprintProps: List<FingerprintSensorPropertiesInternal> = listOf(), + faceProps: List<FaceSensorPropertiesInternal> = listOf() + ) : + AuthContainerView( + Config().apply { + mContext = context + mCallback = callback + mSensorIds = + (fingerprintProps.map { it.sensorId } + faceProps.map { it.sensorId }) + .toIntArray() + mSkipAnimation = true + mPromptInfo = PromptInfo().apply { this.authenticators = authenticators } + }, + fingerprintProps, + faceProps, + wakefulnessLifecycle, + userManager, + lockPatternUtils, + interactionJankMonitor, + Handler(TestableLooper.get(this).looper), + FakeExecutor(FakeSystemClock()) + ) { + override fun postOnAnimation(runnable: Runnable) { + runnable.run() + } + } + + override fun waitForIdleSync() = TestableLooper.get(this).processAllMessages() + + private fun AuthContainerView.addToView() { + ViewUtils.attachView(this) + waitForIdleSync() + assertThat(isAttachedToWindow).isTrue() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt index c85334db9499..90948ff3b769 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt @@ -42,6 +42,8 @@ import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.keyguard.domain.interactor.BouncerInteractor import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.LockscreenShadeTransitionController @@ -103,6 +105,8 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { @Mock private lateinit var udfpsView: UdfpsView @Mock private lateinit var udfpsEnrollView: UdfpsEnrollView @Mock private lateinit var activityLaunchAnimator: ActivityLaunchAnimator + @Mock private lateinit var featureFlags: FeatureFlags + @Mock private lateinit var bouncerInteractor: BouncerInteractor @Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams> private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true } @@ -136,7 +140,8 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { keyguardUpdateMonitor, dialogManager, dumpManager, transitionController, configurationController, systemClock, keyguardStateController, unlockedScreenOffAnimationController, udfpsDisplayMode, REQUEST_ID, reason, - controllerCallback, onTouch, activityLaunchAnimator, isDebuggable + controllerCallback, onTouch, activityLaunchAnimator, featureFlags, + bouncerInteractor, isDebuggable ) block() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index 28e13b8e81ab..be39c0de22a1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -69,7 +69,9 @@ import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.ScreenLifecycle; +import com.android.systemui.keyguard.domain.interactor.BouncerInteractor; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.ShadeExpansionStateManager; @@ -171,6 +173,8 @@ public class UdfpsControllerTest extends SysuiTestCase { private FakeExecutor mFgExecutor; @Mock private UdfpsDisplayMode mUdfpsDisplayMode; + @Mock + private FeatureFlags mFeatureFlags; // Stuff for configuring mocks @Mock @@ -191,6 +195,8 @@ public class UdfpsControllerTest extends SysuiTestCase { private ActivityLaunchAnimator mActivityLaunchAnimator; @Mock private AlternateUdfpsTouchProvider mAlternateTouchProvider; + @Mock + private BouncerInteractor mBouncerInteractor; // Capture listeners so that they can be used to send events @Captor private ArgumentCaptor<IUdfpsOverlayController> mOverlayCaptor; @@ -252,6 +258,7 @@ public class UdfpsControllerTest extends SysuiTestCase { mStatusBarKeyguardViewManager, mDumpManager, mKeyguardUpdateMonitor, + mFeatureFlags, mFalsingManager, mPowerManager, mAccessibilityManager, @@ -270,7 +277,8 @@ public class UdfpsControllerTest extends SysuiTestCase { mLatencyTracker, mActivityLaunchAnimator, Optional.of(mAlternateTouchProvider), - mBiometricsExecutor); + mBiometricsExecutor, + mBouncerInteractor); verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture()); mOverlayController = mOverlayCaptor.getValue(); verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java new file mode 100644 index 000000000000..e5c7a42c06a6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; + +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.animation.ActivityLaunchAnimator; +import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.flags.Flags; +import com.android.systemui.keyguard.KeyguardViewMediator; +import com.android.systemui.keyguard.domain.interactor.BouncerInteractor; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.shade.ShadeExpansionChangeEvent; +import com.android.systemui.shade.ShadeExpansionListener; +import com.android.systemui.shade.ShadeExpansionStateManager; +import com.android.systemui.statusbar.LockscreenShadeTransitionController; +import com.android.systemui.statusbar.phone.KeyguardBouncer; +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; +import com.android.systemui.statusbar.phone.SystemUIDialogManager; +import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController; +import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.concurrency.DelayableExecutor; +import com.android.systemui.util.time.FakeSystemClock; + +import org.junit.Before; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.List; + +public class UdfpsKeyguardViewControllerBaseTest extends SysuiTestCase { + // Dependencies + protected @Mock UdfpsKeyguardView mView; + protected @Mock Context mResourceContext; + protected @Mock StatusBarStateController mStatusBarStateController; + protected @Mock ShadeExpansionStateManager mShadeExpansionStateManager; + protected @Mock StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; + protected @Mock LockscreenShadeTransitionController mLockscreenShadeTransitionController; + protected @Mock DumpManager mDumpManager; + protected @Mock DelayableExecutor mExecutor; + protected @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor; + protected @Mock KeyguardStateController mKeyguardStateController; + protected @Mock KeyguardViewMediator mKeyguardViewMediator; + protected @Mock ConfigurationController mConfigurationController; + protected @Mock UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController; + protected @Mock SystemUIDialogManager mDialogManager; + protected @Mock UdfpsController mUdfpsController; + protected @Mock ActivityLaunchAnimator mActivityLaunchAnimator; + protected @Mock KeyguardBouncer mBouncer; + protected @Mock BouncerInteractor mBouncerInteractor; + + protected FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); + protected FakeSystemClock mSystemClock = new FakeSystemClock(); + + protected UdfpsKeyguardViewController mController; + + // Capture listeners so that they can be used to send events + private @Captor ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerCaptor; + protected StatusBarStateController.StateListener mStatusBarStateListener; + + private @Captor ArgumentCaptor<ShadeExpansionListener> mExpansionListenerCaptor; + protected List<ShadeExpansionListener> mExpansionListeners; + + private @Captor ArgumentCaptor<StatusBarKeyguardViewManager.AlternateAuthInterceptor> + mAltAuthInterceptorCaptor; + protected StatusBarKeyguardViewManager.AlternateAuthInterceptor mAltAuthInterceptor; + + private @Captor ArgumentCaptor<KeyguardStateController.Callback> + mKeyguardStateControllerCallbackCaptor; + protected KeyguardStateController.Callback mKeyguardStateControllerCallback; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mView.getContext()).thenReturn(mResourceContext); + when(mResourceContext.getString(anyInt())).thenReturn("test string"); + when(mKeyguardViewMediator.isAnimatingScreenOff()).thenReturn(false); + when(mView.getUnpausedAlpha()).thenReturn(255); + mController = createUdfpsKeyguardViewController(); + } + + protected void sendStatusBarStateChanged(int statusBarState) { + mStatusBarStateListener.onStateChanged(statusBarState); + } + + protected void captureStatusBarStateListeners() { + verify(mStatusBarStateController).addCallback(mStateListenerCaptor.capture()); + mStatusBarStateListener = mStateListenerCaptor.getValue(); + } + + protected void captureStatusBarExpansionListeners() { + verify(mShadeExpansionStateManager, times(2)) + .addExpansionListener(mExpansionListenerCaptor.capture()); + // first (index=0) is from super class, UdfpsAnimationViewController. + // second (index=1) is from UdfpsKeyguardViewController + mExpansionListeners = mExpansionListenerCaptor.getAllValues(); + } + + protected void updateStatusBarExpansion(float fraction, boolean expanded) { + ShadeExpansionChangeEvent event = + new ShadeExpansionChangeEvent( + fraction, expanded, /* tracking= */ false, /* dragDownPxAmount= */ 0f); + for (ShadeExpansionListener listener : mExpansionListeners) { + listener.onPanelExpansionChanged(event); + } + } + + protected void captureAltAuthInterceptor() { + verify(mStatusBarKeyguardViewManager).setAlternateAuthInterceptor( + mAltAuthInterceptorCaptor.capture()); + mAltAuthInterceptor = mAltAuthInterceptorCaptor.getValue(); + } + + protected void captureKeyguardStateControllerCallback() { + verify(mKeyguardStateController).addCallback( + mKeyguardStateControllerCallbackCaptor.capture()); + mKeyguardStateControllerCallback = mKeyguardStateControllerCallbackCaptor.getValue(); + } + + public UdfpsKeyguardViewController createUdfpsKeyguardViewController() { + return createUdfpsKeyguardViewController(false); + } + + protected UdfpsKeyguardViewController createUdfpsKeyguardViewController( + boolean useModernBouncer) { + mFeatureFlags.set(Flags.MODERN_BOUNCER, useModernBouncer); + when(mStatusBarKeyguardViewManager.getBouncer()).thenReturn( + useModernBouncer ? null : mBouncer); + return new UdfpsKeyguardViewController( + mView, + mStatusBarStateController, + mShadeExpansionStateManager, + mStatusBarKeyguardViewManager, + mKeyguardUpdateMonitor, + mDumpManager, + mLockscreenShadeTransitionController, + mConfigurationController, + mSystemClock, + mKeyguardStateController, + mUnlockedScreenOffAnimationController, + mDialogManager, + mUdfpsController, + mActivityLaunchAnimator, + mFeatureFlags, + mBouncerInteractor); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java index c0f9c82fb131..55b61948ee45 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java @@ -25,125 +25,52 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.eq; 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.content.Context; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper.RunWithLooper; import androidx.test.filters.SmallTest; -import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.animation.ActivityLaunchAnimator; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.keyguard.KeyguardViewMediator; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.shade.ShadeExpansionListener; -import com.android.systemui.shade.ShadeExpansionStateManager; -import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; -import com.android.systemui.statusbar.phone.SystemUIDialogManager; -import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController; -import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.systemui.statusbar.policy.KeyguardStateController; -import com.android.systemui.util.concurrency.DelayableExecutor; -import com.android.systemui.util.time.FakeSystemClock; - -import org.junit.Before; +import com.android.systemui.statusbar.phone.KeyguardBouncer; + import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.List; @SmallTest @RunWith(AndroidTestingRunner.class) @RunWithLooper -public class UdfpsKeyguardViewControllerTest extends SysuiTestCase { - // Dependencies - @Mock - private UdfpsKeyguardView mView; - @Mock - private Context mResourceContext; - @Mock - private StatusBarStateController mStatusBarStateController; - @Mock - private ShadeExpansionStateManager mShadeExpansionStateManager; - @Mock - private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; - @Mock - private LockscreenShadeTransitionController mLockscreenShadeTransitionController; - @Mock - private DumpManager mDumpManager; - @Mock - private DelayableExecutor mExecutor; - @Mock - private KeyguardUpdateMonitor mKeyguardUpdateMonitor; - @Mock - private KeyguardStateController mKeyguardStateController; - @Mock - private KeyguardViewMediator mKeyguardViewMediator; - @Mock - private ConfigurationController mConfigurationController; - @Mock - private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController; - @Mock - private SystemUIDialogManager mDialogManager; - @Mock - private UdfpsController mUdfpsController; - @Mock - private ActivityLaunchAnimator mActivityLaunchAnimator; - private FakeSystemClock mSystemClock = new FakeSystemClock(); - - private UdfpsKeyguardViewController mController; - - // Capture listeners so that they can be used to send events - @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerCaptor; - private StatusBarStateController.StateListener mStatusBarStateListener; - - @Captor private ArgumentCaptor<ShadeExpansionListener> mExpansionListenerCaptor; - private List<ShadeExpansionListener> mExpansionListeners; - - @Captor private ArgumentCaptor<StatusBarKeyguardViewManager.AlternateAuthInterceptor> - mAltAuthInterceptorCaptor; - private StatusBarKeyguardViewManager.AlternateAuthInterceptor mAltAuthInterceptor; - - @Captor private ArgumentCaptor<KeyguardStateController.Callback> - mKeyguardStateControllerCallbackCaptor; - private KeyguardStateController.Callback mKeyguardStateControllerCallback; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - when(mView.getContext()).thenReturn(mResourceContext); - when(mResourceContext.getString(anyInt())).thenReturn("test string"); - when(mKeyguardViewMediator.isAnimatingScreenOff()).thenReturn(false); - when(mView.getUnpausedAlpha()).thenReturn(255); - mController = new UdfpsKeyguardViewController( - mView, - mStatusBarStateController, - mShadeExpansionStateManager, - mStatusBarKeyguardViewManager, - mKeyguardUpdateMonitor, - mDumpManager, - mLockscreenShadeTransitionController, - mConfigurationController, - mSystemClock, - mKeyguardStateController, - mUnlockedScreenOffAnimationController, - mDialogManager, - mUdfpsController, - mActivityLaunchAnimator); +public class UdfpsKeyguardViewControllerTest extends UdfpsKeyguardViewControllerBaseTest { + private @Captor ArgumentCaptor<KeyguardBouncer.BouncerExpansionCallback> + mBouncerExpansionCallbackCaptor; + private KeyguardBouncer.BouncerExpansionCallback mBouncerExpansionCallback; + + @Override + public UdfpsKeyguardViewController createUdfpsKeyguardViewController() { + return createUdfpsKeyguardViewController(/* useModernBouncer */ false); + } + + @Test + public void testShouldPauseAuth_bouncerShowing() { + mController.onViewAttached(); + captureStatusBarStateListeners(); + sendStatusBarStateChanged(StatusBarState.KEYGUARD); + + captureBouncerExpansionCallback(); + when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(true); + when(mStatusBarKeyguardViewManager.bouncerIsOrWillBeShowing()).thenReturn(true); + mBouncerExpansionCallback.onVisibilityChanged(true); + + assertTrue(mController.shouldPauseAuth()); } + + @Test public void testRegistersExpansionChangedListenerOnAttached() { mController.onViewAttached(); @@ -202,20 +129,6 @@ public class UdfpsKeyguardViewControllerTest extends SysuiTestCase { } @Test - public void testShouldPauseAuthBouncerShowing() { - mController.onViewAttached(); - captureStatusBarStateListeners(); - sendStatusBarStateChanged(StatusBarState.KEYGUARD); - - captureAltAuthInterceptor(); - when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(true); - when(mStatusBarKeyguardViewManager.bouncerIsOrWillBeShowing()).thenReturn(true); - mAltAuthInterceptor.onBouncerVisibilityChanged(); - - assertTrue(mController.shouldPauseAuth()); - } - - @Test public void testShouldPauseAuthUnpausedAlpha0() { mController.onViewAttached(); captureStatusBarStateListeners(); @@ -503,41 +416,8 @@ public class UdfpsKeyguardViewControllerTest extends SysuiTestCase { verify(mView, atLeastOnce()).setPauseAuth(false); } - private void sendStatusBarStateChanged(int statusBarState) { - mStatusBarStateListener.onStateChanged(statusBarState); - } - - private void captureStatusBarStateListeners() { - verify(mStatusBarStateController).addCallback(mStateListenerCaptor.capture()); - mStatusBarStateListener = mStateListenerCaptor.getValue(); - } - - private void captureStatusBarExpansionListeners() { - verify(mShadeExpansionStateManager, times(2)) - .addExpansionListener(mExpansionListenerCaptor.capture()); - // first (index=0) is from super class, UdfpsAnimationViewController. - // second (index=1) is from UdfpsKeyguardViewController - mExpansionListeners = mExpansionListenerCaptor.getAllValues(); - } - - private void updateStatusBarExpansion(float fraction, boolean expanded) { - ShadeExpansionChangeEvent event = - new ShadeExpansionChangeEvent( - fraction, expanded, /* tracking= */ false, /* dragDownPxAmount= */ 0f); - for (ShadeExpansionListener listener : mExpansionListeners) { - listener.onPanelExpansionChanged(event); - } - } - - private void captureAltAuthInterceptor() { - verify(mStatusBarKeyguardViewManager).setAlternateAuthInterceptor( - mAltAuthInterceptorCaptor.capture()); - mAltAuthInterceptor = mAltAuthInterceptorCaptor.getValue(); - } - - private void captureKeyguardStateControllerCallback() { - verify(mKeyguardStateController).addCallback( - mKeyguardStateControllerCallbackCaptor.capture()); - mKeyguardStateControllerCallback = mKeyguardStateControllerCallbackCaptor.getValue(); + private void captureBouncerExpansionCallback() { + verify(mBouncer).addBouncerExpansionCallback(mBouncerExpansionCallbackCaptor.capture()); + mBouncerExpansionCallback = mBouncerExpansionCallbackCaptor.getValue(); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt new file mode 100644 index 000000000000..7b1976811868 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt @@ -0,0 +1,102 @@ +/* + * 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.biometrics + +import android.os.Handler +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.keyguard.KeyguardSecurityModel +import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.keyguard.DismissCallbackRegistry +import com.android.systemui.keyguard.data.BouncerView +import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository +import com.android.systemui.keyguard.domain.interactor.BouncerCallbackInteractor +import com.android.systemui.keyguard.domain.interactor.BouncerInteractor +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.phone.KeyguardBouncer +import com.android.systemui.statusbar.phone.KeyguardBypassController +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations + +@RunWith(AndroidTestingRunner::class) +@SmallTest +@TestableLooper.RunWithLooper +class UdfpsKeyguardViewControllerWithCoroutinesTest : UdfpsKeyguardViewControllerBaseTest() { + lateinit var keyguardBouncerRepository: KeyguardBouncerRepository + + @Before + override fun setUp() { + allowTestableLooperAsMainThread() // repeatWhenAttached requires the main thread + MockitoAnnotations.initMocks(this) + keyguardBouncerRepository = + KeyguardBouncerRepository( + mock(com.android.keyguard.ViewMediatorCallback::class.java), + mKeyguardUpdateMonitor + ) + super.setUp() + } + + override fun createUdfpsKeyguardViewController(): UdfpsKeyguardViewController? { + mBouncerInteractor = + BouncerInteractor( + keyguardBouncerRepository, + mock(BouncerView::class.java), + mock(Handler::class.java), + mKeyguardStateController, + mock(KeyguardSecurityModel::class.java), + mock(BouncerCallbackInteractor::class.java), + mock(FalsingCollector::class.java), + mock(DismissCallbackRegistry::class.java), + mock(KeyguardBypassController::class.java), + mKeyguardUpdateMonitor + ) + return createUdfpsKeyguardViewController(/* useModernBouncer */ true) + } + + /** After migration, replaces LockIconViewControllerTest version */ + @Test + fun testShouldPauseAuthBouncerShowing() = + runBlocking(IMMEDIATE) { + // GIVEN view attached and we're on the keyguard + mController.onViewAttached() + captureStatusBarStateListeners() + sendStatusBarStateChanged(StatusBarState.KEYGUARD) + + // WHEN the bouncer expansion is VISIBLE + val job = mController.listenForBouncerExpansion(this) + keyguardBouncerRepository.setVisible(true) + keyguardBouncerRepository.setPanelExpansion(KeyguardBouncer.EXPANSION_VISIBLE) + yield() + + // THEN UDFPS shouldPauseAuth == true + assertTrue(mController.shouldPauseAuth()) + + job.cancel() + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt index 25bc91f413de..eb6e5174d078 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt @@ -40,6 +40,7 @@ import java.util.concurrent.Executor import junit.framework.Assert.assertSame import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runBlockingTest import org.junit.Before import org.junit.Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractorTest.kt index e6c8dd87d982..5743b2f03d3a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractorTest.kt @@ -27,8 +27,8 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.BouncerView +import com.android.systemui.keyguard.data.BouncerViewDelegate import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository -import com.android.systemui.keyguard.shared.model.BouncerCallbackActionsModel import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel import com.android.systemui.plugins.ActivityStarter @@ -57,6 +57,7 @@ class BouncerInteractorTest : SysuiTestCase() { @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var repository: KeyguardBouncerRepository @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var bouncerView: BouncerView + @Mock private lateinit var bouncerViewDelegate: BouncerViewDelegate @Mock private lateinit var keyguardStateController: KeyguardStateController @Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel @Mock private lateinit var bouncerCallbackInteractor: BouncerCallbackInteractor @@ -86,6 +87,7 @@ class BouncerInteractorTest : SysuiTestCase() { ) `when`(repository.startingDisappearAnimation.value).thenReturn(null) `when`(repository.show.value).thenReturn(null) + `when`(bouncerView.delegate).thenReturn(bouncerViewDelegate) } @Test @@ -97,7 +99,7 @@ class BouncerInteractorTest : SysuiTestCase() { verify(repository).setHide(false) verify(repository).setStartingToHide(false) verify(repository).setScrimmed(true) - verify(repository).setExpansion(EXPANSION_VISIBLE) + verify(repository).setPanelExpansion(EXPANSION_VISIBLE) verify(repository).setShowingSoon(true) verify(keyguardStateController).notifyBouncerShowing(true) verify(bouncerCallbackInteractor).dispatchStartingToShow() @@ -108,7 +110,7 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun testShow_isNotScrimmed() { - verify(repository, never()).setExpansion(EXPANSION_VISIBLE) + verify(repository, never()).setPanelExpansion(EXPANSION_VISIBLE) } @Test @@ -124,7 +126,6 @@ class BouncerInteractorTest : SysuiTestCase() { verify(falsingCollector).onBouncerHidden() verify(keyguardStateController).notifyBouncerShowing(false) verify(repository).setShowingSoon(false) - verify(repository).setOnDismissAction(null) verify(repository).setVisible(false) verify(repository).setHide(true) verify(repository).setShow(null) @@ -132,26 +133,26 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun testExpansion() { - `when`(repository.expansionAmount.value).thenReturn(0.5f) - bouncerInteractor.setExpansion(0.6f) - verify(repository).setExpansion(0.6f) + `when`(repository.panelExpansionAmount.value).thenReturn(0.5f) + bouncerInteractor.setPanelExpansion(0.6f) + verify(repository).setPanelExpansion(0.6f) verify(bouncerCallbackInteractor).dispatchExpansionChanged(0.6f) } @Test fun testExpansion_fullyShown() { - `when`(repository.expansionAmount.value).thenReturn(0.5f) + `when`(repository.panelExpansionAmount.value).thenReturn(0.5f) `when`(repository.startingDisappearAnimation.value).thenReturn(null) - bouncerInteractor.setExpansion(EXPANSION_VISIBLE) + bouncerInteractor.setPanelExpansion(EXPANSION_VISIBLE) verify(falsingCollector).onBouncerShown() verify(bouncerCallbackInteractor).dispatchFullyShown() } @Test fun testExpansion_fullyHidden() { - `when`(repository.expansionAmount.value).thenReturn(0.5f) + `when`(repository.panelExpansionAmount.value).thenReturn(0.5f) `when`(repository.startingDisappearAnimation.value).thenReturn(null) - bouncerInteractor.setExpansion(EXPANSION_HIDDEN) + bouncerInteractor.setPanelExpansion(EXPANSION_HIDDEN) verify(repository).setVisible(false) verify(repository).setShow(null) verify(falsingCollector).onBouncerHidden() @@ -161,8 +162,8 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun testExpansion_startingToHide() { - `when`(repository.expansionAmount.value).thenReturn(EXPANSION_VISIBLE) - bouncerInteractor.setExpansion(0.1f) + `when`(repository.panelExpansionAmount.value).thenReturn(EXPANSION_VISIBLE) + bouncerInteractor.setPanelExpansion(0.1f) verify(repository).setStartingToHide(true) verify(bouncerCallbackInteractor).dispatchStartingToHide() } @@ -178,8 +179,7 @@ class BouncerInteractorTest : SysuiTestCase() { val onDismissAction = mock(ActivityStarter.OnDismissAction::class.java) val cancelAction = mock(Runnable::class.java) bouncerInteractor.setDismissAction(onDismissAction, cancelAction) - verify(repository) - .setOnDismissAction(BouncerCallbackActionsModel(onDismissAction, cancelAction)) + verify(bouncerViewDelegate).setDismissAction(onDismissAction, cancelAction) } @Test @@ -234,7 +234,7 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun testIsFullShowing() { `when`(repository.isVisible.value).thenReturn(true) - `when`(repository.expansionAmount.value).thenReturn(EXPANSION_VISIBLE) + `when`(repository.panelExpansionAmount.value).thenReturn(EXPANSION_VISIBLE) `when`(repository.startingDisappearAnimation.value).thenReturn(null) assertThat(bouncerInteractor.isFullyShowing()).isTrue() `when`(repository.isVisible.value).thenReturn(false) @@ -255,7 +255,7 @@ class BouncerInteractorTest : SysuiTestCase() { assertThat(bouncerInteractor.isInTransit()).isTrue() `when`(repository.showingSoon.value).thenReturn(false) assertThat(bouncerInteractor.isInTransit()).isFalse() - `when`(repository.expansionAmount.value).thenReturn(0.5f) + `when`(repository.panelExpansionAmount.value).thenReturn(0.5f) assertThat(bouncerInteractor.isInTransit()).isTrue() } @@ -269,10 +269,9 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun testWillDismissWithAction() { - `when`(repository.onDismissAction.value?.onDismissAction) - .thenReturn(mock(ActivityStarter.OnDismissAction::class.java)) + `when`(bouncerViewDelegate.willDismissWithActions()).thenReturn(true) assertThat(bouncerInteractor.willDismissWithAction()).isTrue() - `when`(repository.onDismissAction.value?.onDismissAction).thenReturn(null) + `when`(bouncerViewDelegate.willDismissWithActions()).thenReturn(false) assertThat(bouncerInteractor.willDismissWithAction()).isFalse() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java index 6adce7a827b6..c1fa9b39f50b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java @@ -61,6 +61,7 @@ import android.view.Display; import android.view.DisplayInfo; import android.view.MotionEvent; import android.view.View; +import android.view.ViewRootImpl; import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.view.WindowManager; @@ -201,6 +202,8 @@ public class NavigationBarTest extends SysuiTestCase { private WakefulnessLifecycle mWakefulnessLifecycle; @Mock private Resources mResources; + @Mock + private ViewRootImpl mViewRootImpl; private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); private DeviceConfigProxyFake mDeviceConfigProxyFake = new DeviceConfigProxyFake(); @@ -227,6 +230,7 @@ public class NavigationBarTest extends SysuiTestCase { when(mUserContextProvider.createCurrentUserContext(any(Context.class))) .thenReturn(mContext); when(mNavigationBarView.getResources()).thenReturn(mResources); + when(mNavigationBarView.getViewRootImpl()).thenReturn(mViewRootImpl); setupSysuiDependency(); // This class inflates views that call Dependency.get, thus these injections are still // necessary. diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierTest.java index 99a17a613041..9115ab3bacca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierTest.java @@ -24,6 +24,7 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.LayoutInflater; import android.view.View; +import android.widget.TextView; import androidx.test.filters.SmallTest; @@ -48,6 +49,7 @@ public class QSCarrierTest extends SysuiTestCase { public void setUp() throws Exception { mTestableLooper = TestableLooper.get(this); LayoutInflater inflater = LayoutInflater.from(mContext); + mContext.ensureTestableResources(); mTestableLooper.runWithLooper(() -> mQSCarrier = (QSCarrier) inflater.inflate(R.layout.qs_carrier, null)); @@ -119,4 +121,30 @@ public class QSCarrierTest extends SysuiTestCase { mQSCarrier.updateState(c, true); assertEquals(View.GONE, mQSCarrier.getRSSIView().getVisibility()); } + + @Test + public void testCarrierNameMaxWidth_smallScreen_fromResource() { + int maxEms = 10; + mContext.getOrCreateTestableResources().addOverride(R.integer.qs_carrier_max_em, maxEms); + mContext.getOrCreateTestableResources() + .addOverride(R.bool.config_use_large_screen_shade_header, false); + TextView carrierText = mQSCarrier.requireViewById(R.id.qs_carrier_text); + + mQSCarrier.onConfigurationChanged(mContext.getResources().getConfiguration()); + + assertEquals(maxEms, carrierText.getMaxEms()); + } + + @Test + public void testCarrierNameMaxWidth_largeScreen_maxInt() { + int maxEms = 10; + mContext.getOrCreateTestableResources().addOverride(R.integer.qs_carrier_max_em, maxEms); + mContext.getOrCreateTestableResources() + .addOverride(R.bool.config_use_large_screen_shade_header, true); + TextView carrierText = mQSCarrier.requireViewById(R.id.qs_carrier_text); + + mQSCarrier.onConfigurationChanged(mContext.getResources().getConfiguration()); + + assertEquals(Integer.MAX_VALUE, carrierText.getMaxEms()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt index 760bb9bec559..081a2181cfe5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt @@ -47,7 +47,11 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -59,6 +63,7 @@ import org.mockito.Mockito.`when` as whenever @RunWithLooper class FooterActionsViewModelTest : SysuiTestCase() { private lateinit var utils: FooterActionsTestUtils + private val testDispatcher = UnconfinedTestDispatcher(TestCoroutineScheduler()) @Before fun setUp() { @@ -130,6 +135,7 @@ class FooterActionsViewModelTest : SysuiTestCase() { showPowerButton = false, footerActionsInteractor = utils.footerActionsInteractor( + bgDispatcher = testDispatcher, userSwitcherRepository = utils.userSwitcherRepository( userTracker = userTracker, @@ -137,6 +143,7 @@ class FooterActionsViewModelTest : SysuiTestCase() { userManager = userManager, userInfoController = userInfoController, userSwitcherController = userSwitcherControllerWrapper.controller, + bgDispatcher = testDispatcher, ), ) ) @@ -217,9 +224,11 @@ class FooterActionsViewModelTest : SysuiTestCase() { footerActionsInteractor = utils.footerActionsInteractor( qsSecurityFooterUtils = qsSecurityFooterUtils, + bgDispatcher = testDispatcher, securityRepository = utils.securityRepository( securityController = securityController, + bgDispatcher = testDispatcher, ), ), ) @@ -288,9 +297,14 @@ class FooterActionsViewModelTest : SysuiTestCase() { footerActionsInteractor = utils.footerActionsInteractor( qsSecurityFooterUtils = qsSecurityFooterUtils, - securityRepository = utils.securityRepository(securityController), + securityRepository = + utils.securityRepository( + securityController, + bgDispatcher = testDispatcher, + ), foregroundServicesRepository = utils.foregroundServicesRepository(fgsManagerController), + bgDispatcher = testDispatcher, ), ) @@ -376,6 +390,7 @@ class FooterActionsViewModelTest : SysuiTestCase() { utils.footerActionsInteractor( qsSecurityFooterUtils = qsSecurityFooterUtils, broadcastDispatcher = broadcastDispatcher, + bgDispatcher = testDispatcher, ), ) @@ -400,4 +415,7 @@ class FooterActionsViewModelTest : SysuiTestCase() { underTest.onVisibilityChangeRequested(visible = true) assertThat(underTest.isVisible.value).isTrue() } + + private fun runBlockingTest(block: suspend TestScope.() -> Unit) = + runTest(testDispatcher) { block() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java index 2c76be64aa7c..b067ee76f3b3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; import android.service.quicksettings.Tile; import android.testing.AndroidTestingRunner; @@ -136,6 +137,20 @@ public class QSIconViewImplTest extends SysuiTestCase { assertEquals(mIconView.getColor(s1), mIconView.getColor(s2)); } + @Test + public void testIconNotAnimatedWhenAllowAnimationsFalse() { + ImageView iv = new ImageView(mContext); + AnimatedVectorDrawable d = mock(AnimatedVectorDrawable.class); + State s = new State(); + s.icon = mock(Icon.class); + when(s.icon.getDrawable(any())).thenReturn(d); + when(s.icon.getInvisibleDrawable(any())).thenReturn(d); + + mIconView.updateIcon(iv, s, false); + + verify(d, never()).start(); + } + private static Drawable.ConstantState fakeConstantState(Drawable otherDrawable) { return new Drawable.ConstantState() { @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java index 26a0770a7bba..a4a7995ae3c8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java @@ -152,7 +152,7 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase { // WHEN showing alt auth, not dozing, drag down helper doesn't want to intercept when(mStatusBarStateController.isDozing()).thenReturn(false); - when(mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()).thenReturn(true); + when(mStatusBarKeyguardViewManager.isShowingAlternateAuth()).thenReturn(true); when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false); // THEN we should intercept touch @@ -165,7 +165,7 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase { // WHEN not showing alt auth, not dozing, drag down helper doesn't want to intercept when(mStatusBarStateController.isDozing()).thenReturn(false); - when(mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()).thenReturn(false); + when(mStatusBarKeyguardViewManager.isShowingAlternateAuth()).thenReturn(false); when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false); // THEN we shouldn't intercept touch @@ -178,7 +178,7 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase { // WHEN showing alt auth, not dozing, drag down helper doesn't want to intercept when(mStatusBarStateController.isDozing()).thenReturn(false); - when(mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()).thenReturn(true); + when(mStatusBarKeyguardViewManager.isShowingAlternateAuth()).thenReturn(true); when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false); // THEN we should handle the touch diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index 421f918a135e..7478e4c1f878 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -22,6 +22,7 @@ import static android.app.NotificationManager.IMPORTANCE_HIGH; import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -60,6 +61,7 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; +import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; @@ -194,6 +196,25 @@ public class NotificationTestHelper { } /** + * Creates a generic row with rounded border. + * + * @return a generic row with the set roundness. + * @throws Exception + */ + public ExpandableNotificationRow createRowWithRoundness( + float topRoundness, + float bottomRoundness, + SourceType sourceType + ) throws Exception { + ExpandableNotificationRow row = createRow(); + row.requestTopRoundness(topRoundness, false, sourceType); + row.requestBottomRoundness(bottomRoundness, /*animate = */ false, sourceType); + assertEquals(topRoundness, row.getTopRoundness(), /* delta = */ 0f); + assertEquals(bottomRoundness, row.getBottomRoundness(), /* delta = */ 0f); + return row; + } + + /** * Creates a generic row. * * @return a generic row with no special properties. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java index 7c41abba6176..438b528944be 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java @@ -25,6 +25,7 @@ import android.view.View; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; @@ -151,4 +152,37 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { Assert.assertNotNull("Children container must have a header after recreation", mChildrenContainer.getCurrentHeaderView()); } + + @Test + public void addNotification_shouldResetOnScrollRoundness() throws Exception { + ExpandableNotificationRow row = mNotificationTestHelper.createRowWithRoundness( + /* topRoundness = */ 1f, + /* bottomRoundness = */ 1f, + /* sourceType = */ SourceType.OnScroll); + + mChildrenContainer.addNotification(row, 0); + + Assert.assertEquals(0f, row.getTopRoundness(), /* delta = */ 0f); + Assert.assertEquals(0f, row.getBottomRoundness(), /* delta = */ 0f); + } + + @Test + public void addNotification_shouldNotResetOtherRoundness() throws Exception { + ExpandableNotificationRow row1 = mNotificationTestHelper.createRowWithRoundness( + /* topRoundness = */ 1f, + /* bottomRoundness = */ 1f, + /* sourceType = */ SourceType.DefaultValue); + ExpandableNotificationRow row2 = mNotificationTestHelper.createRowWithRoundness( + /* topRoundness = */ 1f, + /* bottomRoundness = */ 1f, + /* sourceType = */ SourceType.OnDismissAnimation); + + mChildrenContainer.addNotification(row1, 0); + mChildrenContainer.addNotification(row2, 0); + + Assert.assertEquals(1f, row1.getTopRoundness(), /* delta = */ 0f); + Assert.assertEquals(1f, row1.getBottomRoundness(), /* delta = */ 0f); + Assert.assertEquals(1f, row2.getTopRoundness(), /* delta = */ 0f); + Assert.assertEquals(1f, row2.getBottomRoundness(), /* delta = */ 0f); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt index 77418138158b..bda233611158 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt @@ -1,6 +1,7 @@ package com.android.systemui.statusbar.notification.stack import android.testing.AndroidTestingRunner +import android.testing.TestableLooper import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress @@ -8,8 +9,10 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ShadeInterpolation import com.android.systemui.statusbar.NotificationShelf import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.notification.SourceType import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView +import com.android.systemui.statusbar.notification.row.NotificationTestHelper import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.StackScrollAlgorithmState import com.android.systemui.util.mockito.mock import junit.framework.Assert.assertEquals @@ -37,6 +40,13 @@ class NotificationShelfTest : SysuiTestCase() { private val shelfState = shelf.viewState as NotificationShelf.ShelfState private val ambientState = mock(AmbientState::class.java) private val hostLayoutController: NotificationStackScrollLayoutController = mock() + private val notificationTestHelper by lazy { + allowTestableLooperAsMainThread() + NotificationTestHelper( + mContext, + mDependency, + TestableLooper.get(this)) + } @Before fun setUp() { @@ -299,6 +309,39 @@ class NotificationShelfTest : SysuiTestCase() { ) } + @Test + fun resetOnScrollRoundness_shouldSetOnScrollTo0() { + val row: ExpandableNotificationRow = notificationTestHelper.createRowWithRoundness( + /* topRoundness = */ 1f, + /* bottomRoundness = */ 1f, + /* sourceType = */ SourceType.OnScroll) + + NotificationShelf.resetOnScrollRoundness(row) + + assertEquals(0f, row.topRoundness) + assertEquals(0f, row.bottomRoundness) + } + + @Test + fun resetOnScrollRoundness_shouldNotResetOtherRoundness() { + val row1: ExpandableNotificationRow = notificationTestHelper.createRowWithRoundness( + /* topRoundness = */ 1f, + /* bottomRoundness = */ 1f, + /* sourceType = */ SourceType.DefaultValue) + val row2: ExpandableNotificationRow = notificationTestHelper.createRowWithRoundness( + /* topRoundness = */ 1f, + /* bottomRoundness = */ 1f, + /* sourceType = */ SourceType.OnDismissAnimation) + + NotificationShelf.resetOnScrollRoundness(row1) + NotificationShelf.resetOnScrollRoundness(row2) + + assertEquals(1f, row1.topRoundness) + assertEquals(1f, row1.bottomRoundness) + assertEquals(1f, row2.topRoundness) + assertEquals(1f, row2.bottomRoundness) + } + private fun setFractionToShade(fraction: Float) { whenever(ambientState.fractionToShade).thenReturn(fraction) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt index de1fec85360b..288f54c7d03c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt @@ -17,16 +17,18 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow class FakeMobileConnectionRepository : MobileConnectionRepository { private val _subscriptionsModelFlow = MutableStateFlow(MobileSubscriptionModel()) - override val subscriptionModelFlow: Flow<MobileSubscriptionModel> = _subscriptionsModelFlow + override val subscriptionModelFlow = _subscriptionsModelFlow private val _dataEnabled = MutableStateFlow(true) override val dataEnabled = _dataEnabled + private val _isDefaultDataSubscription = MutableStateFlow(true) + override val isDefaultDataSubscription = _isDefaultDataSubscription + fun setMobileSubscriptionModel(model: MobileSubscriptionModel) { _subscriptionsModelFlow.value = model } @@ -34,4 +36,8 @@ class FakeMobileConnectionRepository : MobileConnectionRepository { fun setDataEnabled(enabled: Boolean) { _dataEnabled.value = enabled } + + fun setIsDefaultDataSubscription(isDefault: Boolean) { + _isDefaultDataSubscription.value = isDefault + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt index 813e750684a0..533d5d9d5b4a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt @@ -17,8 +17,9 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import android.telephony.SubscriptionInfo -import android.telephony.SubscriptionManager +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import com.android.settingslib.mobile.MobileMappings.Config +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -26,18 +27,26 @@ class FakeMobileConnectionsRepository : MobileConnectionsRepository { private val _subscriptionsFlow = MutableStateFlow<List<SubscriptionInfo>>(listOf()) override val subscriptionsFlow: Flow<List<SubscriptionInfo>> = _subscriptionsFlow - private val _activeMobileDataSubscriptionId = - MutableStateFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID) + private val _activeMobileDataSubscriptionId = MutableStateFlow(INVALID_SUBSCRIPTION_ID) override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId private val _defaultDataSubRatConfig = MutableStateFlow(Config()) override val defaultDataSubRatConfig = _defaultDataSubRatConfig + private val _defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID) + override val defaultDataSubId = _defaultDataSubId + + private val _mobileConnectivity = MutableStateFlow(MobileConnectivityModel()) + override val defaultMobileNetworkConnectivity = _mobileConnectivity + private val subIdRepos = mutableMapOf<Int, MobileConnectionRepository>() override fun getRepoForSubId(subId: Int): MobileConnectionRepository { return subIdRepos[subId] ?: FakeMobileConnectionRepository().also { subIdRepos[subId] = it } } + private val _globalMobileDataSettingChangedEvent = MutableStateFlow(Unit) + override val globalMobileDataSettingChangedEvent = _globalMobileDataSettingChangedEvent + fun setSubscriptions(subs: List<SubscriptionInfo>) { _subscriptionsFlow.value = subs } @@ -46,6 +55,18 @@ class FakeMobileConnectionsRepository : MobileConnectionsRepository { _defaultDataSubRatConfig.value = config } + fun setDefaultDataSubId(id: Int) { + _defaultDataSubId.value = id + } + + fun setMobileConnectivity(model: MobileConnectivityModel) { + _mobileConnectivity.value = model + } + + suspend fun triggerGlobalMobileDataSettingChangedEvent() { + _globalMobileDataSettingChangedEvent.emit(Unit) + } + fun setActiveMobileDataSubscriptionId(subId: Int) { _activeMobileDataSubscriptionId.value = subId } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt index 6c495c5c705a..141b50c017e1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt @@ -16,13 +16,12 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow /** Defaults to `true` */ class FakeUserSetupRepository : UserSetupRepository { private val _isUserSetup: MutableStateFlow<Boolean> = MutableStateFlow(true) - override val isUserSetupFlow: Flow<Boolean> = _isUserSetup + override val isUserSetupFlow = _isUserSetup fun setUserSetup(setup: Boolean) { _isUserSetup.value = setup diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt index 093936444789..5ce51bb62c78 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository +import android.os.UserHandle +import android.provider.Settings import android.telephony.CellSignalStrengthCdma import android.telephony.ServiceState import android.telephony.SignalStrength @@ -42,6 +44,7 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -67,16 +70,23 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { @Mock private lateinit var logger: ConnectivityPipelineLogger private val scope = CoroutineScope(IMMEDIATE) + private val globalSettings = FakeSettings() + private val connectionsRepo = FakeMobileConnectionsRepository() @Before fun setUp() { MockitoAnnotations.initMocks(this) + globalSettings.userId = UserHandle.USER_ALL whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID) underTest = MobileConnectionRepositoryImpl( + context, SUB_1_ID, telephonyManager, + globalSettings, + connectionsRepo.defaultDataSubId, + connectionsRepo.globalMobileDataSettingChangedEvent, IMMEDIATE, logger, scope, @@ -290,14 +300,20 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { } @Test - fun dataEnabled_isEnabled() = + fun dataEnabled_initial_false() = runBlocking(IMMEDIATE) { whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true) - var latest: Boolean? = null - val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this) + assertThat(underTest.dataEnabled.value).isFalse() + } - assertThat(latest).isTrue() + @Test + fun dataEnabled_isEnabled_true() = + runBlocking(IMMEDIATE) { + whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true) + val job = underTest.dataEnabled.launchIn(this) + + assertThat(underTest.dataEnabled.value).isTrue() job.cancel() } @@ -306,10 +322,59 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { fun dataEnabled_isDisabled() = runBlocking(IMMEDIATE) { whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false) + val job = underTest.dataEnabled.launchIn(this) + + assertThat(underTest.dataEnabled.value).isFalse() + + job.cancel() + } + + @Test + fun isDefaultDataSubscription_isDefault() = + runBlocking(IMMEDIATE) { + connectionsRepo.setDefaultDataSubId(SUB_1_ID) + + var latest: Boolean? = null + val job = underTest.isDefaultDataSubscription.onEach { latest = it }.launchIn(this) + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun isDefaultDataSubscription_isNotDefault() = + runBlocking(IMMEDIATE) { + // Our subId is SUB_1_ID + connectionsRepo.setDefaultDataSubId(123) + + var latest: Boolean? = null + val job = underTest.isDefaultDataSubscription.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun isDataConnectionAllowed_subIdSettingUpdate_valueUpdated() = + runBlocking(IMMEDIATE) { + val subIdSettingName = "${Settings.Global.MOBILE_DATA}$SUB_1_ID" var latest: Boolean? = null val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this) + // We don't read the setting directly, we query telephony when changes happen + whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false) + globalSettings.putInt(subIdSettingName, 0) + assertThat(latest).isFalse() + + whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true) + globalSettings.putInt(subIdSettingName, 1) + assertThat(latest).isTrue() + + whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false) + globalSettings.putInt(subIdSettingName, 0) assertThat(latest).isFalse() job.cancel() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt index 326e0d28166f..a953a3d802e6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt @@ -16,26 +16,33 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository +import android.content.Intent +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED +import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +import android.provider.Settings import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager import android.telephony.TelephonyCallback import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener import android.telephony.TelephonyManager import androidx.test.filters.SmallTest +import com.android.internal.telephony.PhoneConstants import com.android.systemui.SysuiTestCase -import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking @@ -43,7 +50,6 @@ import org.junit.After import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test -import org.mockito.ArgumentMatchers import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -54,32 +60,26 @@ import org.mockito.MockitoAnnotations class MobileConnectionsRepositoryTest : SysuiTestCase() { private lateinit var underTest: MobileConnectionsRepositoryImpl + @Mock private lateinit var connectivityManager: ConnectivityManager @Mock private lateinit var subscriptionManager: SubscriptionManager @Mock private lateinit var telephonyManager: TelephonyManager @Mock private lateinit var logger: ConnectivityPipelineLogger - @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher private val scope = CoroutineScope(IMMEDIATE) + private val globalSettings = FakeSettings() @Before fun setUp() { MockitoAnnotations.initMocks(this) - whenever( - broadcastDispatcher.broadcastFlow( - any(), - nullable(), - ArgumentMatchers.anyInt(), - nullable(), - ) - ) - .thenReturn(flowOf(Unit)) underTest = MobileConnectionsRepositoryImpl( + connectivityManager, subscriptionManager, telephonyManager, logger, - broadcastDispatcher, + fakeBroadcastDispatcher, + globalSettings, context, IMMEDIATE, scope, @@ -214,6 +214,139 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun testDefaultDataSubId_updatesOnBroadcast() = + runBlocking(IMMEDIATE) { + var latest: Int? = null + val job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this) + + fakeBroadcastDispatcher.registeredReceivers.forEach { receiver -> + receiver.onReceive( + context, + Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) + .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_2_ID) + ) + } + + assertThat(latest).isEqualTo(SUB_2_ID) + + fakeBroadcastDispatcher.registeredReceivers.forEach { receiver -> + receiver.onReceive( + context, + Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) + .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID) + ) + } + + assertThat(latest).isEqualTo(SUB_1_ID) + + job.cancel() + } + + @Test + fun mobileConnectivity_default() { + assertThat(underTest.defaultMobileNetworkConnectivity.value) + .isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false)) + } + + @Test + fun mobileConnectivity_isConnected_isValidated() = + runBlocking(IMMEDIATE) { + val caps = createCapabilities(connected = true, validated = true) + + var latest: MobileConnectivityModel? = null + val job = + underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + + assertThat(latest) + .isEqualTo(MobileConnectivityModel(isConnected = true, isValidated = true)) + + job.cancel() + } + + @Test + fun globalMobileDataSettingsChangedEvent_producesOnSettingChange() = + runBlocking(IMMEDIATE) { + var produced = false + val job = + underTest.globalMobileDataSettingChangedEvent + .onEach { produced = true } + .launchIn(this) + + assertThat(produced).isFalse() + + globalSettings.putInt(Settings.Global.MOBILE_DATA, 0) + + assertThat(produced).isTrue() + + job.cancel() + } + + @Test + fun mobileConnectivity_isConnected_isNotValidated() = + runBlocking(IMMEDIATE) { + val caps = createCapabilities(connected = true, validated = false) + + var latest: MobileConnectivityModel? = null + val job = + underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + + assertThat(latest) + .isEqualTo(MobileConnectivityModel(isConnected = true, isValidated = false)) + + job.cancel() + } + + @Test + fun mobileConnectivity_isNotConnected_isNotValidated() = + runBlocking(IMMEDIATE) { + val caps = createCapabilities(connected = false, validated = false) + + var latest: MobileConnectivityModel? = null + val job = + underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + + assertThat(latest) + .isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false)) + + job.cancel() + } + + /** In practice, I don't think this state can ever happen (!connected, validated) */ + @Test + fun mobileConnectivity_isNotConnected_isValidated() = + runBlocking(IMMEDIATE) { + val caps = createCapabilities(connected = false, validated = true) + + var latest: MobileConnectivityModel? = null + val job = + underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + + assertThat(latest).isEqualTo(MobileConnectivityModel(false, true)) + + job.cancel() + } + + private fun createCapabilities(connected: Boolean, validated: Boolean): NetworkCapabilities = + mock<NetworkCapabilities>().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(connected) + whenever(it.hasCapability(NET_CAPABILITY_VALIDATED)).thenReturn(validated) + } + + private fun getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback { + val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>() + verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture()) + return callbackCaptor.value!! + } + private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener { val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>() verify(subscriptionManager) @@ -242,5 +375,8 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { private const val SUB_2_ID = 2 private val SUB_2 = mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) } + + private const val NET_ID = 123 + private val NETWORK = mock<Network>().apply { whenever(getNetId()).thenReturn(NET_ID) } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt index 5611c448c550..3ae7d3ca1c19 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt @@ -28,18 +28,23 @@ class FakeMobileIconInteractor : MobileIconInteractor { private val _isEmergencyOnly = MutableStateFlow(false) override val isEmergencyOnly = _isEmergencyOnly + private val _isFailedConnection = MutableStateFlow(false) + override val isDefaultConnectionFailed = _isFailedConnection + + override val isDataConnected = MutableStateFlow(true) + private val _isDataEnabled = MutableStateFlow(true) override val isDataEnabled = _isDataEnabled + private val _isDefaultDataEnabled = MutableStateFlow(true) + override val isDefaultDataEnabled = _isDefaultDataEnabled + private val _level = MutableStateFlow(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) override val level = _level private val _numberOfLevels = MutableStateFlow(4) override val numberOfLevels = _numberOfLevels - private val _cutOut = MutableStateFlow(false) - override val cutOut = _cutOut - fun setIconGroup(group: SignalIcon.MobileIconGroup) { _iconGroup.value = group } @@ -52,6 +57,14 @@ class FakeMobileIconInteractor : MobileIconInteractor { _isDataEnabled.value = enabled } + fun setIsDefaultDataEnabled(disabled: Boolean) { + _isDefaultDataEnabled.value = disabled + } + + fun setIsFailedConnection(failed: Boolean) { + _isFailedConnection.value = failed + } + fun setLevel(level: Int) { _level.value = level } @@ -59,8 +72,4 @@ class FakeMobileIconInteractor : MobileIconInteractor { fun setNumberOfLevels(num: Int) { _numberOfLevels.value = num } - - fun setCutOut(cutOut: Boolean) { - _cutOut.value = cutOut - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt index 2bd228603cb0..061c3b54650e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt @@ -26,8 +26,7 @@ import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import kotlinx.coroutines.flow.MutableStateFlow -class FakeMobileIconsInteractor(private val mobileMappings: MobileMappingsProxy) : - MobileIconsInteractor { +class FakeMobileIconsInteractor(mobileMappings: MobileMappingsProxy) : MobileIconsInteractor { val THREE_G_KEY = mobileMappings.toIconKey(THREE_G) val LTE_KEY = mobileMappings.toIconKey(LTE) val FOUR_G_KEY = mobileMappings.toIconKey(FOUR_G) @@ -46,9 +45,14 @@ class FakeMobileIconsInteractor(private val mobileMappings: MobileMappingsProxy) FIVE_G_OVERRIDE_KEY to TelephonyIcons.NR_5G, ) + override val isDefaultConnectionFailed = MutableStateFlow(false) + private val _filteredSubscriptions = MutableStateFlow<List<SubscriptionInfo>>(listOf()) override val filteredSubscriptions = _filteredSubscriptions + private val _activeDataConnectionHasDataEnabled = MutableStateFlow(false) + override val activeDataConnectionHasDataEnabled = _activeDataConnectionHasDataEnabled + private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING) override val defaultMobileIconMapping = _defaultMobileIconMapping diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index ff44af4c9204..7fc1c0f6272c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -23,6 +23,7 @@ import androidx.test.filters.SmallTest import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType @@ -34,6 +35,7 @@ import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsPro import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -49,12 +51,17 @@ class MobileIconInteractorTest : SysuiTestCase() { private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy) private val connectionRepository = FakeMobileConnectionRepository() + private val scope = CoroutineScope(IMMEDIATE) + @Before fun setUp() { underTest = MobileIconInteractorImpl( + scope, + mobileIconsInteractor.activeDataConnectionHasDataEnabled, mobileIconsInteractor.defaultMobileIconMapping, mobileIconsInteractor.defaultMobileIconGroup, + mobileIconsInteractor.isDefaultConnectionFailed, mobileMappingsProxy, connectionRepository, ) @@ -196,6 +203,66 @@ class MobileIconInteractorTest : SysuiTestCase() { job.cancel() } + @Test + fun test_isDefaultDataEnabled_matchesParent() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isDefaultDataEnabled.onEach { latest = it }.launchIn(this) + + mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = true + assertThat(latest).isTrue() + + mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = false + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun test_isDefaultConnectionFailed_matchedParent() = + runBlocking(IMMEDIATE) { + val job = underTest.isDefaultConnectionFailed.launchIn(this) + + mobileIconsInteractor.isDefaultConnectionFailed.value = false + assertThat(underTest.isDefaultConnectionFailed.value).isFalse() + + mobileIconsInteractor.isDefaultConnectionFailed.value = true + assertThat(underTest.isDefaultConnectionFailed.value).isTrue() + + job.cancel() + } + + @Test + fun dataState_connected() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this) + + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel(dataConnectionState = DataConnectionState.Connected) + ) + yield() + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun dataState_notConnected() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this) + + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel(dataConnectionState = DataConnectionState.Disconnected) + ) + + assertThat(latest).isFalse() + + job.cancel() + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt index 877ce0e6b351..b56dcd752557 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt @@ -17,8 +17,10 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository @@ -32,6 +34,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield import org.junit.After import org.junit.Before import org.junit.Test @@ -168,6 +171,92 @@ class MobileIconsInteractorTest : SysuiTestCase() { job.cancel() } + @Test + fun activeDataConnection_turnedOn() = + runBlocking(IMMEDIATE) { + CONNECTION_1.setDataEnabled(true) + var latest: Boolean? = null + val job = + underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this) + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun activeDataConnection_turnedOff() = + runBlocking(IMMEDIATE) { + CONNECTION_1.setDataEnabled(true) + var latest: Boolean? = null + val job = + underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this) + + CONNECTION_1.setDataEnabled(false) + yield() + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun activeDataConnection_invalidSubId() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = + underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this) + + connectionsRepository.setActiveMobileDataSubscriptionId(INVALID_SUBSCRIPTION_ID) + yield() + + // An invalid active subId should tell us that data is off + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun failedConnection_connected_validated_notFailed() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this) + connectionsRepository.setMobileConnectivity(MobileConnectivityModel(true, true)) + yield() + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun failedConnection_notConnected_notValidated_notFailed() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this) + + connectionsRepository.setMobileConnectivity(MobileConnectivityModel(false, false)) + yield() + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun failedConnection_connected_notValidated_failed() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this) + + connectionsRepository.setMobileConnectivity(MobileConnectivityModel(true, false)) + yield() + + assertThat(latest).isTrue() + + job.cancel() + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt index ce0f33f400ab..d4c2c3f6cc2b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -46,10 +46,12 @@ class MobileIconViewModelTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) interactor.apply { setLevel(1) - setCutOut(false) + setIsDefaultDataEnabled(true) + setIsFailedConnection(false) setIconGroup(THREE_G) setIsEmergencyOnly(false) setNumberOfLevels(4) + isDataConnected.value = true } underTest = MobileIconViewModel(SUB_1_ID, interactor, logger) } @@ -59,8 +61,23 @@ class MobileIconViewModelTest : SysuiTestCase() { runBlocking(IMMEDIATE) { var latest: Int? = null val job = underTest.iconId.onEach { latest = it }.launchIn(this) + val expected = defaultSignal() - assertThat(latest).isEqualTo(SignalDrawable.getState(1, 4, false)) + assertThat(latest).isEqualTo(expected) + + job.cancel() + } + + @Test + fun iconId_cutout_whenDefaultDataDisabled() = + runBlocking(IMMEDIATE) { + interactor.setIsDefaultDataEnabled(false) + + var latest: Int? = null + val job = underTest.iconId.onEach { latest = it }.launchIn(this) + val expected = defaultSignal(level = 1, connected = false) + + assertThat(latest).isEqualTo(expected) job.cancel() } @@ -97,6 +114,44 @@ class MobileIconViewModelTest : SysuiTestCase() { } @Test + fun networkType_nullWhenFailedConnection() = + runBlocking(IMMEDIATE) { + interactor.setIconGroup(THREE_G) + interactor.setIsDataEnabled(true) + interactor.setIsFailedConnection(true) + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + assertThat(latest).isNull() + + job.cancel() + } + + @Test + fun networkType_nullWhenDataDisconnects() = + runBlocking(IMMEDIATE) { + val initial = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription) + ) + + interactor.setIconGroup(THREE_G) + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + interactor.setIconGroup(THREE_G) + assertThat(latest).isEqualTo(initial) + + interactor.isDataConnected.value = false + yield() + + assertThat(latest).isNull() + + job.cancel() + } + + @Test fun networkType_null_changeToDisabled() = runBlocking(IMMEDIATE) { val expected = @@ -119,6 +174,14 @@ class MobileIconViewModelTest : SysuiTestCase() { job.cancel() } + /** Convenience constructor for these tests */ + private fun defaultSignal( + level: Int = 1, + connected: Boolean = true, + ): Int { + return SignalDrawable.getState(level, /* numLevels */ 4, !connected) + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate private const val SUB_1_ID = 1 diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt index 97571b23be56..f682e31c0547 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt @@ -44,6 +44,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.advanceUntilIdle import org.junit.Before import org.junit.Test import org.junit.runner.RunWith diff --git a/services/core/java/com/android/server/wm/Dimmer.java b/services/core/java/com/android/server/wm/Dimmer.java index e7ab63eab202..13a1cb6daf38 100644 --- a/services/core/java/com/android/server/wm/Dimmer.java +++ b/services/core/java/com/android/server/wm/Dimmer.java @@ -216,14 +216,10 @@ class Dimmer { return; } - if (container != null) { - // The dim method is called from WindowState.prepareSurfaces(), which is always called - // in the correct Z from lowest Z to highest. This ensures that the dim layer is always - // relative to the highest Z layer with a dim. - t.setRelativeLayer(d.mDimLayer, container.getSurfaceControl(), relativeLayer); - } else { - t.setLayer(d.mDimLayer, Integer.MAX_VALUE); - } + // The dim method is called from WindowState.prepareSurfaces(), which is always called + // in the correct Z from lowest Z to highest. This ensures that the dim layer is always + // relative to the highest Z layer with a dim. + t.setRelativeLayer(d.mDimLayer, container.getSurfaceControl(), relativeLayer); t.setAlpha(d.mDimLayer, alpha); t.setBackgroundBlurRadius(d.mDimLayer, blurRadius); @@ -231,32 +227,6 @@ class Dimmer { } /** - * Finish a dim started by dimAbove in the case there was no call to dimAbove. - * - * @param t A Transaction in which to finish the dim. - */ - void stopDim(SurfaceControl.Transaction t) { - if (mDimState != null) { - t.hide(mDimState.mDimLayer); - mDimState.isVisible = false; - mDimState.mDontReset = false; - } - } - - /** - * Place a Dim above the entire host container. The caller is responsible for calling stopDim to - * remove this effect. If the Dim can be assosciated with a particular child of the host - * consider using the other variant of dimAbove which ties the Dim lifetime to the child - * lifetime more explicitly. - * - * @param t A transaction in which to apply the Dim. - * @param alpha The alpha at which to Dim. - */ - void dimAbove(SurfaceControl.Transaction t, float alpha) { - dim(t, null, 1, alpha, 0); - } - - /** * Place a dim above the given container, which should be a child of the host container. * for each call to {@link WindowContainer#prepareSurfaces} the Dim state will be reset * and the child should call dimAbove again to request the Dim to continue. diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 42a3ec6abbc5..2688ff757f64 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -81,11 +81,7 @@ import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ANIM; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_SCREEN_ON; import static com.android.server.policy.PhoneWindowManager.TOAST_WINDOW_TIMEOUT; -import static com.android.server.policy.WindowManagerPolicy.TRANSIT_ENTER; -import static com.android.server.policy.WindowManagerPolicy.TRANSIT_EXIT; -import static com.android.server.policy.WindowManagerPolicy.TRANSIT_HIDE; import static com.android.server.policy.WindowManagerPolicy.TRANSIT_PREVIEW_DONE; -import static com.android.server.policy.WindowManagerPolicy.TRANSIT_SHOW; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.LID_ABSENT; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; @@ -1439,90 +1435,6 @@ public class DisplayPolicy { */ int selectAnimation(WindowState win, int transit) { ProtoLog.i(WM_DEBUG_ANIM, "selectAnimation in %s: transit=%d", win, transit); - if (win == mStatusBar) { - if (transit == TRANSIT_EXIT - || transit == TRANSIT_HIDE) { - return R.anim.dock_top_exit; - } else if (transit == TRANSIT_ENTER - || transit == TRANSIT_SHOW) { - return R.anim.dock_top_enter; - } - } else if (win == mNavigationBar) { - if (win.getAttrs().windowAnimations != 0) { - return ANIMATION_STYLEABLE; - } - // This can be on either the bottom or the right or the left. - if (mNavigationBarPosition == NAV_BAR_BOTTOM) { - if (transit == TRANSIT_EXIT - || transit == TRANSIT_HIDE) { - if (mService.mPolicy.isKeyguardShowingAndNotOccluded()) { - return R.anim.dock_bottom_exit_keyguard; - } else { - return R.anim.dock_bottom_exit; - } - } else if (transit == TRANSIT_ENTER - || transit == TRANSIT_SHOW) { - return R.anim.dock_bottom_enter; - } - } else if (mNavigationBarPosition == NAV_BAR_RIGHT) { - if (transit == TRANSIT_EXIT - || transit == TRANSIT_HIDE) { - return R.anim.dock_right_exit; - } else if (transit == TRANSIT_ENTER - || transit == TRANSIT_SHOW) { - return R.anim.dock_right_enter; - } - } else if (mNavigationBarPosition == NAV_BAR_LEFT) { - if (transit == TRANSIT_EXIT - || transit == TRANSIT_HIDE) { - return R.anim.dock_left_exit; - } else if (transit == TRANSIT_ENTER - || transit == TRANSIT_SHOW) { - return R.anim.dock_left_enter; - } - } - } else if (win == mStatusBarAlt || win == mNavigationBarAlt || win == mClimateBarAlt - || win == mExtraNavBarAlt) { - if (win.getAttrs().windowAnimations != 0) { - return ANIMATION_STYLEABLE; - } - - int pos = (win == mStatusBarAlt) ? mStatusBarAltPosition : mNavigationBarAltPosition; - - boolean isExitOrHide = transit == TRANSIT_EXIT || transit == TRANSIT_HIDE; - boolean isEnterOrShow = transit == TRANSIT_ENTER || transit == TRANSIT_SHOW; - - switch (pos) { - case ALT_BAR_LEFT: - if (isExitOrHide) { - return R.anim.dock_left_exit; - } else if (isEnterOrShow) { - return R.anim.dock_left_enter; - } - break; - case ALT_BAR_RIGHT: - if (isExitOrHide) { - return R.anim.dock_right_exit; - } else if (isEnterOrShow) { - return R.anim.dock_right_enter; - } - break; - case ALT_BAR_BOTTOM: - if (isExitOrHide) { - return R.anim.dock_bottom_exit; - } else if (isEnterOrShow) { - return R.anim.dock_bottom_enter; - } - break; - case ALT_BAR_TOP: - if (isExitOrHide) { - return R.anim.dock_top_exit; - } else if (isEnterOrShow) { - return R.anim.dock_top_enter; - } - break; - } - } if (transit == TRANSIT_PREVIEW_DONE) { if (win.hasAppShownWindows()) { diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index a8d13c57ffa2..eaa08fd5eb0b 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -1578,7 +1578,9 @@ public class DisplayRotation { false /* forceRelayout */); } else { // Revert the rotation to our saved value if we transition from HALF_FOLDED. - mRotation = mHalfFoldSavedRotation; + if (mHalfFoldSavedRotation != -1) { + mRotation = mHalfFoldSavedRotation; + } // Tell the device to update its orientation (mFoldState is still HALF_FOLDED here // so we will override USER_ROTATION_LOCKED and allow a rotation). mService.updateRotation(false /* alwaysSendConfiguration */, diff --git a/services/core/java/com/android/server/wm/LockTaskController.java b/services/core/java/com/android/server/wm/LockTaskController.java index f11c2a7da840..dcb7fe3fbc8b 100644 --- a/services/core/java/com/android/server/wm/LockTaskController.java +++ b/services/core/java/com/android/server/wm/LockTaskController.java @@ -604,7 +604,10 @@ public class LockTaskController { getDevicePolicyManager().notifyLockTaskModeChanged(false, null, userId); } if (oldLockTaskModeState == LOCK_TASK_MODE_PINNED) { - getStatusBarService().showPinningEnterExitToast(false /* entering */); + final IStatusBarService statusBarService = getStatusBarService(); + if (statusBarService != null) { + statusBarService.showPinningEnterExitToast(false /* entering */); + } } mWindowManager.onLockTaskStateChanged(mLockTaskModeState); } catch (RemoteException ex) { @@ -619,7 +622,10 @@ public class LockTaskController { void showLockTaskToast() { if (mLockTaskModeState == LOCK_TASK_MODE_PINNED) { try { - getStatusBarService().showPinningEscapeToast(); + final IStatusBarService statusBarService = getStatusBarService(); + if (statusBarService != null) { + statusBarService.showPinningEscapeToast(); + } } catch (RemoteException e) { Slog.e(TAG, "Failed to send pinning escape toast", e); } @@ -727,7 +733,10 @@ public class LockTaskController { // When lock task starts, we disable the status bars. try { if (lockTaskModeState == LOCK_TASK_MODE_PINNED) { - getStatusBarService().showPinningEnterExitToast(true /* entering */); + final IStatusBarService statusBarService = getStatusBarService(); + if (statusBarService != null) { + statusBarService.showPinningEnterExitToast(true /* entering */); + } } mWindowManager.onLockTaskStateChanged(lockTaskModeState); mLockTaskModeState = lockTaskModeState; diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 91a755526264..4d29c4d0a134 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -879,8 +879,8 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe void abort() { // This calls back into itself via controller.abort, so just early return here. if (mState == STATE_ABORT) return; - if (mState != STATE_COLLECTING) { - throw new IllegalStateException("Too late to abort."); + if (mState != STATE_COLLECTING && mState != STATE_STARTED) { + throw new IllegalStateException("Too late to abort. state=" + mState); } ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Aborting Transition: %d", mSyncId); mState = STATE_ABORT; diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index ac720be90563..9c9d751244e6 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -8684,11 +8684,12 @@ public class WindowManagerService extends IWindowManager.Stub h.ownerPid = callingPid; if (region == null) { - h.replaceTouchableRegionWithCrop = true; + h.replaceTouchableRegionWithCrop(null); } else { h.touchableRegion.set(region); + h.replaceTouchableRegionWithCrop = false; + h.setTouchableRegionCrop(surface); } - h.setTouchableRegionCrop(null /* use the input surface's bounds */); final SurfaceControl.Transaction t = mTransactionFactory.get(); t.setInputWindowInfo(surface, h); diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java index 5c0557f2d1f4..6e16b5de1d85 100644 --- a/services/core/java/com/android/server/wm/WindowStateAnimator.java +++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java @@ -602,11 +602,17 @@ class WindowStateAnimator { return true; } - final boolean isImeWindow = mWin.mAttrs.type == TYPE_INPUT_METHOD; - if (isEntrance && isImeWindow) { + if (mWin.mAttrs.type == TYPE_INPUT_METHOD) { mWin.getDisplayContent().adjustForImeIfNeeded(); - mWin.setDisplayLayoutNeeded(); - mService.mWindowPlacerLocked.requestTraversal(); + if (isEntrance) { + mWin.setDisplayLayoutNeeded(); + mService.mWindowPlacerLocked.requestTraversal(); + } + } + + if (mWin.mControllableInsetProvider != null) { + // All our animations should be driven by the insets control target. + return false; } // Only apply an animation if the display isn't frozen. If it is @@ -654,14 +660,10 @@ class WindowStateAnimator { Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); mAnimationIsEntrance = isEntrance; } - } else if (!isImeWindow) { + } else { mWin.cancelAnimation(); } - if (!isEntrance && isImeWindow) { - mWin.getDisplayContent().adjustForImeIfNeeded(); - } - return mWin.isAnimating(0 /* flags */, ANIMATION_TYPE_WINDOW_ANIMATION); } diff --git a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java index 55a7c1ba0bca..befe4e85a7de 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java @@ -24,7 +24,6 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_DIMMER; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -139,34 +138,12 @@ public class DimmerTests extends WindowTestsBase { } @Test - public void testDimAboveNoChildCreatesSurface() { - final float alpha = 0.8f; - mDimmer.dimAbove(mTransaction, alpha); - - SurfaceControl dimLayer = getDimLayer(); - - assertNotNull("Dimmer should have created a surface", dimLayer); - - verify(mTransaction).setAlpha(dimLayer, alpha); - verify(mTransaction).setLayer(dimLayer, Integer.MAX_VALUE); - } - - @Test - public void testDimAboveNoChildRedundantlyUpdatesAlphaOnExistingSurface() { - float alpha = 0.8f; - mDimmer.dimAbove(mTransaction, alpha); - final SurfaceControl firstSurface = getDimLayer(); - - alpha = 0.9f; - mDimmer.dimAbove(mTransaction, alpha); - - assertEquals(firstSurface, getDimLayer()); - verify(mTransaction).setAlpha(firstSurface, 0.9f); - } - - @Test public void testUpdateDimsAppliesCrop() { - mDimmer.dimAbove(mTransaction, 0.8f); + TestWindowContainer child = new TestWindowContainer(mWm); + mHost.addChild(child, 0); + + final float alpha = 0.8f; + mDimmer.dimAbove(mTransaction, child, alpha); int width = 100; int height = 300; @@ -178,17 +155,6 @@ public class DimmerTests extends WindowTestsBase { } @Test - public void testDimAboveNoChildNotReset() { - mDimmer.dimAbove(mTransaction, 0.8f); - SurfaceControl dimLayer = getDimLayer(); - mDimmer.resetDimStates(); - - mDimmer.updateDims(mTransaction, new Rect()); - verify(mTransaction).show(getDimLayer()); - verify(mTransaction, never()).remove(dimLayer); - } - - @Test public void testDimAboveWithChildCreatesSurfaceAboveChild() { TestWindowContainer child = new TestWindowContainer(mWm); mHost.addChild(child, 0); |