diff options
Diffstat (limited to 'libs')
41 files changed, 5410 insertions, 40 deletions
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index 843b17703676..16b87c43fc58 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -12,14 +12,99 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Begin ProtoLog +java_library { + name: "wm_shell_protolog-groups", + srcs: [ + "src/com/android/wm/shell/protolog/ShellProtoLogGroup.java", + ":protolog-common-src", + ], +} + +filegroup { + name: "wm_shell-sources", + srcs: ["src/**/*.java"], + path: "src", +} + +genrule { + name: "wm_shell_protolog_src", + srcs: [ + ":wm_shell_protolog-groups", + ":wm_shell-sources", + ], + tools: ["protologtool"], + cmd: "$(location protologtool) transform-protolog-calls " + + "--protolog-class com.android.internal.protolog.common.ProtoLog " + + "--protolog-impl-class com.android.wm.shell.protolog.ShellProtoLogImpl " + + "--protolog-cache-class com.android.wm.shell.protolog.ShellProtoLogCache " + + "--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " + + "--loggroups-jar $(location :wm_shell_protolog-groups) " + + "--output-srcjar $(out) " + + "$(locations :wm_shell-sources)", + out: ["wm_shell_protolog.srcjar"], +} + +genrule { + name: "generate-wm_shell_protolog.json", + srcs: [ + ":wm_shell_protolog-groups", + ":wm_shell-sources", + ], + tools: ["protologtool"], + cmd: "$(location protologtool) generate-viewer-config " + + "--protolog-class com.android.internal.protolog.common.ProtoLog " + + "--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " + + "--loggroups-jar $(location :wm_shell_protolog-groups) " + + "--viewer-conf $(out) " + + "$(locations :wm_shell-sources)", + out: ["wm_shell_protolog.json"], +} + +filegroup { + name: "wm_shell_protolog.json", + srcs: ["res/raw/wm_shell_protolog.json"], +} + +genrule { + name: "checked-wm_shell_protolog.json", + srcs: [ + ":generate-wm_shell_protolog.json", + ":wm_shell_protolog.json", + ], + cmd: "cp $(location :generate-wm_shell_protolog.json) $(out) && " + + "{ ! (diff $(out) $(location :wm_shell_protolog.json) | grep -q '^<') || " + + "{ echo -e '\\n\\n################################################################\\n#\\n" + + "# ERROR: ProtoLog viewer config is stale. To update it, run:\\n#\\n" + + "# cp $(location :generate-wm_shell_protolog.json) " + + "$(location :wm_shell_protolog.json)\\n#\\n" + + "################################################################\\n\\n' >&2 && false; } }", + out: ["wm_shell_protolog.json"], +} +// End ProtoLog + +java_library { + name: "WindowManager-Shell-proto", + + srcs: ["proto/*.proto"], + + proto: { + type: "nano", + }, +} + android_library { name: "WindowManager-Shell", srcs: [ - "src/**/*.java", + ":wm_shell_protolog_src", "src/**/I*.aidl", ], resource_dirs: [ "res", ], + static_libs: [ + "protolog-lib", + "WindowManager-Shell-proto", + ], manifest: "AndroidManifest.xml", -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/proto/wm_shell_trace.proto b/libs/WindowManager/Shell/proto/wm_shell_trace.proto new file mode 100644 index 000000000000..b9e72525f32b --- /dev/null +++ b/libs/WindowManager/Shell/proto/wm_shell_trace.proto @@ -0,0 +1,27 @@ +/* + * 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. + */ + +syntax = "proto2"; + +package com.android.wm.shell; + +option java_multiple_files = true; + +message WmShellTraceProto { + + // Not used, just a test value + optional bool test_value = 1; +} diff --git a/libs/WindowManager/Shell/res/anim/forced_resizable_enter.xml b/libs/WindowManager/Shell/res/anim/forced_resizable_enter.xml new file mode 100644 index 000000000000..01b8fdbe4437 --- /dev/null +++ b/libs/WindowManager/Shell/res/anim/forced_resizable_enter.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2016 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 + --> +<alpha xmlns:android="http://schemas.android.com/apk/res/android" + android:fromAlpha="0.0" + android:toAlpha="1.0" + android:interpolator="@android:interpolator/linear_out_slow_in" + android:duration="280" /> diff --git a/libs/WindowManager/Shell/res/anim/forced_resizable_exit.xml b/libs/WindowManager/Shell/res/anim/forced_resizable_exit.xml new file mode 100644 index 000000000000..6f316a75dbed --- /dev/null +++ b/libs/WindowManager/Shell/res/anim/forced_resizable_exit.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2016 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 + --> +<alpha xmlns:android="http://schemas.android.com/apk/res/android" + android:fromAlpha="1.0" + android:toAlpha="0.0" + android:duration="160" + android:interpolator="@android:interpolator/fast_out_linear_in" + android:zAdjustment="top"/>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/divider.xml b/libs/WindowManager/Shell/res/layout/divider.xml new file mode 100644 index 000000000000..f1f0df054240 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/divider.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2017 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<View xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="72dp" + android:layout_height="1dp" + android:layout_marginTop="8dp" + android:background="?android:attr/colorForeground" + android:alpha="?android:attr/disabledAlpha" /> diff --git a/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml b/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml new file mode 100644 index 000000000000..ad870252d819 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2015 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.splitscreen.DividerView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="match_parent" + android:layout_width="match_parent"> + + <View + style="@style/DockedDividerBackground" + android:id="@+id/docked_divider_background" + android:background="@color/docked_divider_background"/> + + <com.android.wm.shell.splitscreen.MinimizedDockShadow + style="@style/DockedDividerMinimizedShadow" + android:id="@+id/minimized_dock_shadow" + android:alpha="0"/>"> + + <com.android.wm.shell.splitscreen.DividerHandleView + style="@style/DockedDividerHandle" + android:id="@+id/docked_divider_handle" + android:contentDescription="@string/accessibility_divider" + android:background="@null"/> + +</com.android.wm.shell.splitscreen.DividerView> diff --git a/libs/WindowManager/Shell/res/layout/forced_resizable_activity.xml b/libs/WindowManager/Shell/res/layout/forced_resizable_activity.xml new file mode 100644 index 000000000000..3c778c431a2e --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/forced_resizable_activity.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2016 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 + --> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <include + layout="@*android:layout/transient_notification" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center"/> +</FrameLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/pip_menu_activity.xml b/libs/WindowManager/Shell/res/layout/pip_menu.xml index 2e0a5e09e34f..2e0a5e09e34f 100644 --- a/libs/WindowManager/Shell/res/layout/pip_menu_activity.xml +++ b/libs/WindowManager/Shell/res/layout/pip_menu.xml diff --git a/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json b/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json new file mode 100644 index 000000000000..7242793580f9 --- /dev/null +++ b/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json @@ -0,0 +1,46 @@ +{ + "version": "1.0.0", + "messages": { + "-1340279385": { + "message": "Remove listener=%s", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" + }, + "-880817403": { + "message": "Task vanished taskId=%d", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" + }, + "-460572385": { + "message": "Task appeared taskId=%d", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" + }, + "-242812822": { + "message": "Add listener for modes=%s listener=%s", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" + }, + "157713005": { + "message": "Task info changed taskId=%d", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" + }, + "980952660": { + "message": "Task root back pressed taskId=%d", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" + } + }, + "groups": { + "WM_SHELL_TASK_ORG": { + "tag": "WindowManagerShell" + } + } +} diff --git a/libs/WindowManager/Shell/res/values-land/dimens.xml b/libs/WindowManager/Shell/res/values-land/dimens.xml new file mode 100644 index 000000000000..77a601ddf440 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-land/dimens.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * 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. +*/ +--> +<resources> + <dimen name="docked_divider_handle_width">2dp</dimen> + <dimen name="docked_divider_handle_height">16dp</dimen> +</resources>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values-land/styles.xml b/libs/WindowManager/Shell/res/values-land/styles.xml new file mode 100644 index 000000000000..863bb69d4034 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-land/styles.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <style name="DockedDividerBackground"> + <item name="android:layout_width">10dp</item> + <item name="android:layout_height">match_parent</item> + <item name="android:layout_gravity">center_horizontal</item> + </style> + + <style name="DockedDividerHandle"> + <item name="android:layout_gravity">center_vertical</item> + <item name="android:layout_width">48dp</item> + <item name="android:layout_height">96dp</item> + </style> + + <style name="DockedDividerMinimizedShadow"> + <item name="android:layout_width">8dp</item> + <item name="android:layout_height">match_parent</item> + </style> +</resources> + diff --git a/libs/WindowManager/Shell/res/values-sw600dp/config.xml b/libs/WindowManager/Shell/res/values-sw600dp/config.xml new file mode 100644 index 000000000000..f194532f1e0d --- /dev/null +++ b/libs/WindowManager/Shell/res/values-sw600dp/config.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* +** Copyright 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. +*/ +--> + +<!-- These resources are around just to allow their values to be customized + for different hardware and product builds. --> +<resources> + <!-- Animation duration when using long press on recents to dock --> + <integer name="long_press_dock_anim_duration">290</integer> +</resources>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml new file mode 100644 index 000000000000..6a19083e3788 --- /dev/null +++ b/libs/WindowManager/Shell/res/values/colors.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright 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. + */ +--> +<resources> + <color name="docked_divider_background">#ff000000</color> + <color name="docked_divider_handle">#ffffff</color> + <drawable name="forced_resizable_background">#59000000</drawable> + <color name="minimize_dock_shadow_start">#60000000</color> + <color name="minimize_dock_shadow_end">#00000000</color> +</resources>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index 245c0725c2a8..39efd0768eaa 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -26,4 +26,7 @@ <!-- Allow PIP to enable round corner, see also R.dimen.pip_corner_radius --> <bool name="config_pipEnableRoundCorner">false</bool> + + <!-- Animation duration when using long press on recents to dock --> + <integer name="long_press_dock_anim_duration">250</integer> </resources> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 1c1217681b9f..ce690281b491 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -56,4 +56,10 @@ <dimen name="pip_resize_handle_size">12dp</dimen> <dimen name="pip_resize_handle_margin">4dp</dimen> <dimen name="pip_resize_handle_padding">0dp</dimen> + + <!-- How high we lift the divider when touching --> + <dimen name="docked_stack_divider_lift_elevation">4dp</dimen> + + <dimen name="docked_divider_handle_width">16dp</dimen> + <dimen name="docked_divider_handle_height">2dp</dimen> </resources> diff --git a/libs/WindowManager/Shell/res/values/ids.xml b/libs/WindowManager/Shell/res/values/ids.xml index ed20398f309d..fb892388cf74 100644 --- a/libs/WindowManager/Shell/res/values/ids.xml +++ b/libs/WindowManager/Shell/res/values/ids.xml @@ -16,4 +16,11 @@ --> <resources> <item type="id" name="action_pip_resize" /> + + <!-- Accessibility actions for the docked stack divider --> + <item type="id" name="action_move_tl_full" /> + <item type="id" name="action_move_tl_70" /> + <item type="id" name="action_move_tl_50" /> + <item type="id" name="action_move_tl_30" /> + <item type="id" name="action_move_rb_full" /> </resources> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 6752b56fcdf3..cad924771cd3 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -53,4 +53,39 @@ <!-- TODO Deprecated. Label for PIP the drag to dismiss hint. DO NOT TRANSLATE [CHAR LIMIT=NONE]--> <string name="pip_phone_dismiss_hint">Drag down to dismiss</string> + + <!-- Multi-Window strings --> + <!-- Text that gets shown on top of current activity to inform the user that the system force-resized the current activity to be displayed in split-screen and that things might crash/not work properly [CHAR LIMIT=NONE] --> + <string name="dock_forced_resizable">App may not work with split-screen.</string> + <!-- Warning message when we try to dock a non-resizeable task and launch it in fullscreen instead. --> + <string name="dock_non_resizeble_failed_to_dock_text">App does not support split-screen.</string> + <!-- Text that gets shown on top of current activity to inform the user that the system force-resized the current activity to be displayed on a secondary display and that things might crash/not work properly [CHAR LIMIT=NONE] --> + <string name="forced_resizable_secondary_display">App may not work on a secondary display.</string> + <!-- Warning message when we try to launch a non-resizeable activity on a secondary display and launch it on the primary instead. --> + <string name="activity_launch_on_secondary_display_failed_text">App does not support launch on secondary displays.</string> + + <!-- Accessibility label for the divider that separates the windows in split-screen mode [CHAR LIMIT=NONE] --> + <string name="accessibility_divider">Split-screen divider</string> + + <!-- Accessibility action for moving docked stack divider to make the left screen full screen [CHAR LIMIT=NONE] --> + <string name="accessibility_action_divider_left_full">Left full screen</string> + <!-- Accessibility action for moving docked stack divider to make the left screen 70% [CHAR LIMIT=NONE] --> + <string name="accessibility_action_divider_left_70">Left 70%</string> + <!-- Accessibility action for moving docked stack divider to make the left screen 50% [CHAR LIMIT=NONE] --> + <string name="accessibility_action_divider_left_50">Left 50%</string> + <!-- Accessibility action for moving docked stack divider to make the left screen 30% [CHAR LIMIT=NONE] --> + <string name="accessibility_action_divider_left_30">Left 30%</string> + <!-- Accessibility action for moving docked stack divider to make the right screen full screen [CHAR LIMIT=NONE] --> + <string name="accessibility_action_divider_right_full">Right full screen</string> + + <!-- Accessibility action for moving docked stack divider to make the top screen full screen [CHAR LIMIT=NONE] --> + <string name="accessibility_action_divider_top_full">Top full screen</string> + <!-- Accessibility action for moving docked stack divider to make the top screen 70% [CHAR LIMIT=NONE] --> + <string name="accessibility_action_divider_top_70">Top 70%</string> + <!-- Accessibility action for moving docked stack divider to make the top screen 50% [CHAR LIMIT=NONE] --> + <string name="accessibility_action_divider_top_50">Top 50%</string> + <!-- Accessibility action for moving docked stack divider to make the top screen 30% [CHAR LIMIT=NONE] --> + <string name="accessibility_action_divider_top_30">Top 30%</string> + <!-- Accessibility action for moving docked stack divider to make the bottom screen full screen [CHAR LIMIT=NONE] --> + <string name="accessibility_action_divider_bottom_full">Bottom full screen</string> </resources> diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml new file mode 100644 index 000000000000..fffcd33f7992 --- /dev/null +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- Theme used for the activity that shows when the system forced an app to be resizable --> + <style name="ForcedResizableTheme" parent="@android:style/Theme.Translucent.NoTitleBar"> + <item name="android:windowBackground">@drawable/forced_resizable_background</item> + <item name="android:statusBarColor">@android:color/transparent</item> + <item name="android:windowAnimationStyle">@style/Animation.ForcedResizable</item> + </style> + + <style name="Animation.ForcedResizable" parent="@android:style/Animation"> + <item name="android:activityOpenEnterAnimation">@anim/forced_resizable_enter</item> + + <!-- If the target stack doesn't have focus, we do a task to front animation. --> + <item name="android:taskToFrontEnterAnimation">@anim/forced_resizable_enter</item> + <item name="android:activityCloseExitAnimation">@anim/forced_resizable_exit</item> + </style> + + <style name="DockedDividerBackground"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">10dp</item> + <item name="android:layout_gravity">center_vertical</item> + </style> + + <style name="DockedDividerMinimizedShadow"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">8dp</item> + </style> + + <style name="DockedDividerHandle"> + <item name="android:layout_gravity">center_horizontal</item> + <item name="android:layout_width">96dp</item> + <item name="android:layout_height">48dp</item> + </style> +</resources> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index 126374829a18..f9ba695c8503 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -20,17 +20,20 @@ import android.app.ActivityManager.RunningTaskInfo; import android.util.Log; import android.util.Pair; import android.util.SparseArray; -import android.util.SparseIntArray; -import android.view.Surface; import android.view.SurfaceControl; +import android.window.ITaskOrganizerController; import android.window.TaskOrganizer; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + import java.util.ArrayList; -import java.util.LinkedList; -import java.util.concurrent.CopyOnWriteArrayList; +import java.util.Arrays; /** * Unified task organizer for all components in the shell. + * TODO(b/167582004): may consider consolidating this class and TaskOrganizer */ public class ShellTaskOrganizer extends TaskOrganizer { @@ -53,10 +56,21 @@ public class ShellTaskOrganizer extends TaskOrganizer { // require us to report to both old and new listeners) private final SparseArray<Pair<RunningTaskInfo, SurfaceControl>> mTasks = new SparseArray<>(); + public ShellTaskOrganizer() { + super(); + } + + @VisibleForTesting + ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController) { + super(taskOrganizerController); + } + /** * Adds a listener for tasks in a specific windowing mode. */ public void addListener(TaskListener listener, int... windowingModes) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Add listener for modes=%s listener=%s", + Arrays.toString(windowingModes), listener); for (int winMode : windowingModes) { ArrayList<TaskListener> listeners = mListenersByWindowingMode.get(winMode); if (listeners == null) { @@ -84,6 +98,7 @@ public class ShellTaskOrganizer extends TaskOrganizer { * Removes a registered listener. */ public void removeListener(TaskListener listener) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Remove listener=%s", listener); for (int i = 0; i < mListenersByWindowingMode.size(); i++) { mListenersByWindowingMode.valueAt(i).remove(listener); } @@ -91,6 +106,8 @@ public class ShellTaskOrganizer extends TaskOrganizer { @Override public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Task appeared taskId=%d", + taskInfo.taskId); mTasks.put(taskInfo.taskId, new Pair<>(taskInfo, leash)); ArrayList<TaskListener> listeners = mListenersByWindowingMode.get( getWindowingMode(taskInfo)); @@ -103,6 +120,8 @@ public class ShellTaskOrganizer extends TaskOrganizer { @Override public void onTaskInfoChanged(RunningTaskInfo taskInfo) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Task info changed taskId=%d", + taskInfo.taskId); Pair<RunningTaskInfo, SurfaceControl> data = mTasks.get(taskInfo.taskId); int winMode = getWindowingMode(taskInfo); int prevWinMode = getWindowingMode(data.first); @@ -134,6 +153,8 @@ public class ShellTaskOrganizer extends TaskOrganizer { @Override public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Task root back pressed taskId=%d", + taskInfo.taskId); ArrayList<TaskListener> listeners = mListenersByWindowingMode.get( getWindowingMode(taskInfo)); if (listeners != null) { @@ -145,6 +166,8 @@ public class ShellTaskOrganizer extends TaskOrganizer { @Override public void onTaskVanished(RunningTaskInfo taskInfo) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Task vanished taskId=%d", + taskInfo.taskId); int prevWinMode = getWindowingMode(mTasks.get(taskInfo.taskId).first); mTasks.remove(taskInfo.taskId); ArrayList<TaskListener> listeners = mListenersByWindowingMode.get(prevWinMode); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/FlingAnimationUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/FlingAnimationUtils.java new file mode 100644 index 000000000000..357f777e1270 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/FlingAnimationUtils.java @@ -0,0 +1,423 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.animation; + +import android.animation.Animator; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.ViewPropertyAnimator; +import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; + +/** + * Utility class to calculate general fling animation when the finger is released. + */ +public class FlingAnimationUtils { + + private static final String TAG = "FlingAnimationUtils"; + + private static final float LINEAR_OUT_SLOW_IN_X2 = 0.35f; + private static final float LINEAR_OUT_SLOW_IN_X2_MAX = 0.68f; + private static final float LINEAR_OUT_FASTER_IN_X2 = 0.5f; + private static final float LINEAR_OUT_FASTER_IN_Y2_MIN = 0.4f; + private static final float LINEAR_OUT_FASTER_IN_Y2_MAX = 0.5f; + private static final float MIN_VELOCITY_DP_PER_SECOND = 250; + private static final float HIGH_VELOCITY_DP_PER_SECOND = 3000; + + private static final float LINEAR_OUT_SLOW_IN_START_GRADIENT = 0.75f; + private final float mSpeedUpFactor; + private final float mY2; + + private float mMinVelocityPxPerSecond; + private float mMaxLengthSeconds; + private float mHighVelocityPxPerSecond; + private float mLinearOutSlowInX2; + + private AnimatorProperties mAnimatorProperties = new AnimatorProperties(); + private PathInterpolator mInterpolator; + private float mCachedStartGradient = -1; + private float mCachedVelocityFactor = -1; + + public FlingAnimationUtils(DisplayMetrics displayMetrics, float maxLengthSeconds) { + this(displayMetrics, maxLengthSeconds, 0.0f); + } + + /** + * @param maxLengthSeconds the longest duration an animation can become in seconds + * @param speedUpFactor a factor from 0 to 1 how much the slow down should be shifted towards + * the end of the animation. 0 means it's at the beginning and no + * acceleration will take place. + */ + public FlingAnimationUtils(DisplayMetrics displayMetrics, float maxLengthSeconds, + float speedUpFactor) { + this(displayMetrics, maxLengthSeconds, speedUpFactor, -1.0f, 1.0f); + } + + /** + * @param maxLengthSeconds the longest duration an animation can become in seconds + * @param speedUpFactor a factor from 0 to 1 how much the slow down should be shifted towards + * the end of the animation. 0 means it's at the beginning and no + * acceleration will take place. + * @param x2 the x value to take for the second point of the bezier spline. If a + * value below 0 is provided, the value is automatically calculated. + * @param y2 the y value to take for the second point of the bezier spline + */ + public FlingAnimationUtils(DisplayMetrics displayMetrics, float maxLengthSeconds, + float speedUpFactor, float x2, float y2) { + mMaxLengthSeconds = maxLengthSeconds; + mSpeedUpFactor = speedUpFactor; + if (x2 < 0) { + mLinearOutSlowInX2 = interpolate(LINEAR_OUT_SLOW_IN_X2, + LINEAR_OUT_SLOW_IN_X2_MAX, + mSpeedUpFactor); + } else { + mLinearOutSlowInX2 = x2; + } + mY2 = y2; + + mMinVelocityPxPerSecond = MIN_VELOCITY_DP_PER_SECOND * displayMetrics.density; + mHighVelocityPxPerSecond = HIGH_VELOCITY_DP_PER_SECOND * displayMetrics.density; + } + + /** + * Applies the interpolator and length to the animator, such that the fling animation is + * consistent with the finger motion. + * + * @param animator the animator to apply + * @param currValue the current value + * @param endValue the end value of the animator + * @param velocity the current velocity of the motion + */ + public void apply(Animator animator, float currValue, float endValue, float velocity) { + apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue)); + } + + /** + * Applies the interpolator and length to the animator, such that the fling animation is + * consistent with the finger motion. + * + * @param animator the animator to apply + * @param currValue the current value + * @param endValue the end value of the animator + * @param velocity the current velocity of the motion + */ + public void apply(ViewPropertyAnimator animator, float currValue, float endValue, + float velocity) { + apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue)); + } + + /** + * Applies the interpolator and length to the animator, such that the fling animation is + * consistent with the finger motion. + * + * @param animator the animator to apply + * @param currValue the current value + * @param endValue the end value of the animator + * @param velocity the current velocity of the motion + * @param maxDistance the maximum distance for this interaction; the maximum animation length + * gets multiplied by the ratio between the actual distance and this value + */ + public void apply(Animator animator, float currValue, float endValue, float velocity, + float maxDistance) { + AnimatorProperties properties = getProperties(currValue, endValue, velocity, + maxDistance); + animator.setDuration(properties.mDuration); + animator.setInterpolator(properties.mInterpolator); + } + + /** + * Applies the interpolator and length to the animator, such that the fling animation is + * consistent with the finger motion. + * + * @param animator the animator to apply + * @param currValue the current value + * @param endValue the end value of the animator + * @param velocity the current velocity of the motion + * @param maxDistance the maximum distance for this interaction; the maximum animation length + * gets multiplied by the ratio between the actual distance and this value + */ + public void apply(ViewPropertyAnimator animator, float currValue, float endValue, + float velocity, float maxDistance) { + AnimatorProperties properties = getProperties(currValue, endValue, velocity, + maxDistance); + animator.setDuration(properties.mDuration); + animator.setInterpolator(properties.mInterpolator); + } + + private AnimatorProperties getProperties(float currValue, + float endValue, float velocity, float maxDistance) { + float maxLengthSeconds = (float) (mMaxLengthSeconds + * Math.sqrt(Math.abs(endValue - currValue) / maxDistance)); + float diff = Math.abs(endValue - currValue); + float velAbs = Math.abs(velocity); + float velocityFactor = mSpeedUpFactor == 0.0f + ? 1.0f : Math.min(velAbs / HIGH_VELOCITY_DP_PER_SECOND, 1.0f); + float startGradient = interpolate(LINEAR_OUT_SLOW_IN_START_GRADIENT, + mY2 / mLinearOutSlowInX2, velocityFactor); + float durationSeconds = startGradient * diff / velAbs; + Interpolator slowInInterpolator = getInterpolator(startGradient, velocityFactor); + if (durationSeconds <= maxLengthSeconds) { + mAnimatorProperties.mInterpolator = slowInInterpolator; + } else if (velAbs >= mMinVelocityPxPerSecond) { + + // Cross fade between fast-out-slow-in and linear interpolator with current velocity. + durationSeconds = maxLengthSeconds; + VelocityInterpolator velocityInterpolator = new VelocityInterpolator( + durationSeconds, velAbs, diff); + InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator( + velocityInterpolator, slowInInterpolator, Interpolators.LINEAR_OUT_SLOW_IN); + mAnimatorProperties.mInterpolator = superInterpolator; + } else { + + // Just use a normal interpolator which doesn't take the velocity into account. + durationSeconds = maxLengthSeconds; + mAnimatorProperties.mInterpolator = Interpolators.FAST_OUT_SLOW_IN; + } + mAnimatorProperties.mDuration = (long) (durationSeconds * 1000); + return mAnimatorProperties; + } + + private Interpolator getInterpolator(float startGradient, float velocityFactor) { + if (Float.isNaN(velocityFactor)) { + Log.e(TAG, "Invalid velocity factor", new Throwable()); + return Interpolators.LINEAR_OUT_SLOW_IN; + } + if (startGradient != mCachedStartGradient + || velocityFactor != mCachedVelocityFactor) { + float speedup = mSpeedUpFactor * (1.0f - velocityFactor); + float x1 = speedup; + float y1 = speedup * startGradient; + float x2 = mLinearOutSlowInX2; + float y2 = mY2; + try { + mInterpolator = new PathInterpolator(x1, y1, x2, y2); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Illegal path with " + + "x1=" + x1 + " y1=" + y1 + " x2=" + x2 + " y2=" + y2, e); + } + mCachedStartGradient = startGradient; + mCachedVelocityFactor = velocityFactor; + } + return mInterpolator; + } + + /** + * Applies the interpolator and length to the animator, such that the fling animation is + * consistent with the finger motion for the case when the animation is making something + * disappear. + * + * @param animator the animator to apply + * @param currValue the current value + * @param endValue the end value of the animator + * @param velocity the current velocity of the motion + * @param maxDistance the maximum distance for this interaction; the maximum animation length + * gets multiplied by the ratio between the actual distance and this value + */ + public void applyDismissing(Animator animator, float currValue, float endValue, + float velocity, float maxDistance) { + AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity, + maxDistance); + animator.setDuration(properties.mDuration); + animator.setInterpolator(properties.mInterpolator); + } + + /** + * Applies the interpolator and length to the animator, such that the fling animation is + * consistent with the finger motion for the case when the animation is making something + * disappear. + * + * @param animator the animator to apply + * @param currValue the current value + * @param endValue the end value of the animator + * @param velocity the current velocity of the motion + * @param maxDistance the maximum distance for this interaction; the maximum animation length + * gets multiplied by the ratio between the actual distance and this value + */ + public void applyDismissing(ViewPropertyAnimator animator, float currValue, float endValue, + float velocity, float maxDistance) { + AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity, + maxDistance); + animator.setDuration(properties.mDuration); + animator.setInterpolator(properties.mInterpolator); + } + + private AnimatorProperties getDismissingProperties(float currValue, float endValue, + float velocity, float maxDistance) { + float maxLengthSeconds = (float) (mMaxLengthSeconds + * Math.pow(Math.abs(endValue - currValue) / maxDistance, 0.5f)); + float diff = Math.abs(endValue - currValue); + float velAbs = Math.abs(velocity); + float y2 = calculateLinearOutFasterInY2(velAbs); + + float startGradient = y2 / LINEAR_OUT_FASTER_IN_X2; + Interpolator mLinearOutFasterIn = new PathInterpolator(0, 0, LINEAR_OUT_FASTER_IN_X2, y2); + float durationSeconds = startGradient * diff / velAbs; + if (durationSeconds <= maxLengthSeconds) { + mAnimatorProperties.mInterpolator = mLinearOutFasterIn; + } else if (velAbs >= mMinVelocityPxPerSecond) { + + // Cross fade between linear-out-faster-in and linear interpolator with current + // velocity. + durationSeconds = maxLengthSeconds; + VelocityInterpolator velocityInterpolator = new VelocityInterpolator( + durationSeconds, velAbs, diff); + InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator( + velocityInterpolator, mLinearOutFasterIn, Interpolators.LINEAR_OUT_SLOW_IN); + mAnimatorProperties.mInterpolator = superInterpolator; + } else { + + // Just use a normal interpolator which doesn't take the velocity into account. + durationSeconds = maxLengthSeconds; + mAnimatorProperties.mInterpolator = Interpolators.FAST_OUT_LINEAR_IN; + } + mAnimatorProperties.mDuration = (long) (durationSeconds * 1000); + return mAnimatorProperties; + } + + /** + * Calculates the y2 control point for a linear-out-faster-in path interpolator depending on the + * velocity. The faster the velocity, the more "linear" the interpolator gets. + * + * @param velocity the velocity of the gesture. + * @return the y2 control point for a cubic bezier path interpolator + */ + private float calculateLinearOutFasterInY2(float velocity) { + float t = (velocity - mMinVelocityPxPerSecond) + / (mHighVelocityPxPerSecond - mMinVelocityPxPerSecond); + t = Math.max(0, Math.min(1, t)); + return (1 - t) * LINEAR_OUT_FASTER_IN_Y2_MIN + t * LINEAR_OUT_FASTER_IN_Y2_MAX; + } + + /** + * @return the minimum velocity a gesture needs to have to be considered a fling + */ + public float getMinVelocityPxPerSecond() { + return mMinVelocityPxPerSecond; + } + + /** + * An interpolator which interpolates two interpolators with an interpolator. + */ + private static final class InterpolatorInterpolator implements Interpolator { + + private Interpolator mInterpolator1; + private Interpolator mInterpolator2; + private Interpolator mCrossfader; + + InterpolatorInterpolator(Interpolator interpolator1, Interpolator interpolator2, + Interpolator crossfader) { + mInterpolator1 = interpolator1; + mInterpolator2 = interpolator2; + mCrossfader = crossfader; + } + + @Override + public float getInterpolation(float input) { + float t = mCrossfader.getInterpolation(input); + return (1 - t) * mInterpolator1.getInterpolation(input) + + t * mInterpolator2.getInterpolation(input); + } + } + + /** + * An interpolator which interpolates with a fixed velocity. + */ + private static final class VelocityInterpolator implements Interpolator { + + private float mDurationSeconds; + private float mVelocity; + private float mDiff; + + private VelocityInterpolator(float durationSeconds, float velocity, float diff) { + mDurationSeconds = durationSeconds; + mVelocity = velocity; + mDiff = diff; + } + + @Override + public float getInterpolation(float input) { + float time = input * mDurationSeconds; + return time * mVelocity / mDiff; + } + } + + private static class AnimatorProperties { + Interpolator mInterpolator; + long mDuration; + } + + /** Builder for {@link #FlingAnimationUtils}. */ + public static class Builder { + private final DisplayMetrics mDisplayMetrics; + float mMaxLengthSeconds; + float mSpeedUpFactor; + float mX2; + float mY2; + + public Builder(DisplayMetrics displayMetrics) { + mDisplayMetrics = displayMetrics; + reset(); + } + + /** Sets the longest duration an animation can become in seconds. */ + public Builder setMaxLengthSeconds(float maxLengthSeconds) { + mMaxLengthSeconds = maxLengthSeconds; + return this; + } + + /** + * Sets the factor for how much the slow down should be shifted towards the end of the + * animation. + */ + public Builder setSpeedUpFactor(float speedUpFactor) { + mSpeedUpFactor = speedUpFactor; + return this; + } + + /** Sets the x value to take for the second point of the bezier spline. */ + public Builder setX2(float x2) { + mX2 = x2; + return this; + } + + /** Sets the y value to take for the second point of the bezier spline. */ + public Builder setY2(float y2) { + mY2 = y2; + return this; + } + + /** Resets all parameters of the builder. */ + public Builder reset() { + mMaxLengthSeconds = 0; + mSpeedUpFactor = 0.0f; + mX2 = -1.0f; + mY2 = 1.0f; + + return this; + } + + /** Builds {@link #FlingAnimationUtils}. */ + public FlingAnimationUtils build() { + return new FlingAnimationUtils(mDisplayMetrics, mMaxLengthSeconds, mSpeedUpFactor, + mX2, mY2); + } + } + + private static float interpolate(float start, float end, float amount) { + return start * (1.0f - amount) + end * amount; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java new file mode 100644 index 000000000000..b794b91568fc --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java @@ -0,0 +1,45 @@ +/* + * 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.wm.shell.animation; + +import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; + +/** + * Common interpolators used in wm shell library. + */ +public class Interpolators { + /** + * Interpolator for fast out linear in animation. + */ + public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f); + + /** + * Interpolator for fast out slow in animation. + */ + public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f); + + /** + * Interpolator for linear out slow in animation. + */ + public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f); + + /** + * Interpolator to be used when animating a move based on a click. Pair with enough duration. + */ + public static final Interpolator TOUCH_RESPONSE = new PathInterpolator(0.3f, 0f, 0.1f, 1f); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java new file mode 100644 index 000000000000..9cb125087cd9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java @@ -0,0 +1,176 @@ +/* + * 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.wm.shell.common; + +import android.annotation.NonNull; +import android.os.Handler; +import android.util.Slog; +import android.view.SurfaceControl; +import android.window.WindowContainerTransaction; +import android.window.WindowContainerTransactionCallback; +import android.window.WindowOrganizer; + +import java.util.ArrayList; + +/** + * Helper for serializing sync-transactions and corresponding callbacks. + */ +public final class SyncTransactionQueue { + private static final boolean DEBUG = false; + private static final String TAG = "SyncTransactionQueue"; + + // Just a little longer than the sync-engine timeout of 5s + private static final int REPLY_TIMEOUT = 5300; + + private final TransactionPool mTransactionPool; + private final Handler mHandler; + + // Sync Transactions currently don't support nesting or interleaving properly, so + // queue up transactions to run them serially. + private final ArrayList<SyncCallback> mQueue = new ArrayList<>(); + + private SyncCallback mInFlight = null; + private final ArrayList<TransactionRunnable> mRunnables = new ArrayList<>(); + + private final Runnable mOnReplyTimeout = () -> { + synchronized (mQueue) { + if (mInFlight != null && mQueue.contains(mInFlight)) { + Slog.w(TAG, "Sync Transaction timed-out: " + mInFlight.mWCT); + mInFlight.onTransactionReady(mInFlight.mId, new SurfaceControl.Transaction()); + } + } + }; + + public SyncTransactionQueue(TransactionPool pool, Handler handler) { + mTransactionPool = pool; + mHandler = handler; + } + + /** + * Queues a sync transaction to be sent serially to WM. + */ + public void queue(WindowContainerTransaction wct) { + SyncCallback cb = new SyncCallback(wct); + synchronized (mQueue) { + if (DEBUG) Slog.d(TAG, "Queueing up " + wct); + mQueue.add(cb); + if (mQueue.size() == 1) { + cb.send(); + } + } + } + + /** + * Queues a sync transaction only if there are already sync transaction(s) queued or in flight. + * Otherwise just returns without queueing. + * @return {@code true} if queued, {@code false} if not. + */ + public boolean queueIfWaiting(WindowContainerTransaction wct) { + synchronized (mQueue) { + if (mQueue.isEmpty()) { + if (DEBUG) Slog.d(TAG, "Nothing in queue, so skip queueing up " + wct); + return false; + } + if (DEBUG) Slog.d(TAG, "Queue is non-empty, so queueing up " + wct); + SyncCallback cb = new SyncCallback(wct); + mQueue.add(cb); + if (mQueue.size() == 1) { + cb.send(); + } + } + return true; + } + + /** + * Runs a runnable in sync with sync transactions (ie. when the current in-flight transaction + * returns. If there are no transactions in-flight, runnable executes immediately. + */ + public void runInSync(TransactionRunnable runnable) { + synchronized (mQueue) { + if (DEBUG) Slog.d(TAG, "Run in sync. mInFlight=" + mInFlight); + if (mInFlight != null) { + mRunnables.add(runnable); + return; + } + } + SurfaceControl.Transaction t = mTransactionPool.acquire(); + runnable.runWithTransaction(t); + t.apply(); + mTransactionPool.release(t); + } + + // Synchronized on mQueue + private void onTransactionReceived(@NonNull SurfaceControl.Transaction t) { + if (DEBUG) Slog.d(TAG, " Running " + mRunnables.size() + " sync runnables"); + for (int i = 0, n = mRunnables.size(); i < n; ++i) { + mRunnables.get(i).runWithTransaction(t); + } + mRunnables.clear(); + t.apply(); + t.close(); + } + + /** Task to run with transaction. */ + public interface TransactionRunnable { + /** Runs with transaction. */ + void runWithTransaction(SurfaceControl.Transaction t); + } + + private class SyncCallback extends WindowContainerTransactionCallback { + int mId = -1; + final WindowContainerTransaction mWCT; + + SyncCallback(WindowContainerTransaction wct) { + mWCT = wct; + } + + // Must be sychronized on mQueue + void send() { + if (mInFlight != null) { + throw new IllegalStateException("Sync Transactions must be serialized. In Flight: " + + mInFlight.mId + " - " + mInFlight.mWCT); + } + mInFlight = this; + if (DEBUG) Slog.d(TAG, "Sending sync transaction: " + mWCT); + mId = new WindowOrganizer().applySyncTransaction(mWCT, this); + if (DEBUG) Slog.d(TAG, " Sent sync transaction. Got id=" + mId); + mHandler.postDelayed(mOnReplyTimeout, REPLY_TIMEOUT); + } + + @Override + public void onTransactionReady(int id, + @NonNull SurfaceControl.Transaction t) { + mHandler.post(() -> { + synchronized (mQueue) { + if (mId != id) { + Slog.e(TAG, "Got an unexpected onTransactionReady. Expected " + + mId + " but got " + id); + return; + } + mInFlight = null; + mHandler.removeCallbacks(mOnReplyTimeout); + if (DEBUG) Slog.d(TAG, "onTransactionReady id=" + mId); + mQueue.remove(this); + onTransactionReceived(t); + if (!mQueue.isEmpty()) { + mQueue.get(0).send(); + } + } + }); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java new file mode 100644 index 000000000000..ae0975467e3f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java @@ -0,0 +1,92 @@ +/* + * 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.wm.shell.protolog; + +import com.android.internal.protolog.common.IProtoLogGroup; + +/** + * Defines logging groups for ProtoLog. + * + * This file is used by the ProtoLogTool to generate optimized logging code. + */ +public enum ShellProtoLogGroup implements IProtoLogGroup { + WM_SHELL_TASK_ORG(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM_SHELL), + TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest"); + + private final boolean mEnabled; + private volatile boolean mLogToProto; + private volatile boolean mLogToLogcat; + private final String mTag; + + /** + * @param enabled set to false to exclude all log statements for this group from + * compilation, + * they will not be available in runtime. + * @param logToProto enable binary logging for the group + * @param logToLogcat enable text logging for the group + * @param tag name of the source of the logged message + */ + ShellProtoLogGroup(boolean enabled, boolean logToProto, boolean logToLogcat, String tag) { + this.mEnabled = enabled; + this.mLogToProto = logToProto; + this.mLogToLogcat = logToLogcat; + this.mTag = tag; + } + + @Override + public boolean isEnabled() { + return mEnabled; + } + + @Override + public boolean isLogToProto() { + return mLogToProto; + } + + @Override + public boolean isLogToLogcat() { + return mLogToLogcat; + } + + @Override + public boolean isLogToAny() { + return mLogToLogcat || mLogToProto; + } + + @Override + public String getTag() { + return mTag; + } + + @Override + public void setLogToProto(boolean logToProto) { + this.mLogToProto = logToProto; + } + + @Override + public void setLogToLogcat(boolean logToLogcat) { + this.mLogToLogcat = logToLogcat; + } + + private static class Consts { + private static final String TAG_WM_SHELL = "WindowManagerShell"; + + private static final boolean ENABLE_DEBUG = true; + private static final boolean ENABLE_LOG_TO_PROTO_DEBUG = true; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java new file mode 100644 index 000000000000..6a925e74e847 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java @@ -0,0 +1,138 @@ +/* + * 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.wm.shell.protolog; + +import android.annotation.Nullable; +import android.content.Context; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.BaseProtoLogImpl; +import com.android.internal.protolog.ProtoLogViewerConfigReader; +import com.android.internal.protolog.common.IProtoLogGroup; +import com.android.wm.shell.R; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; + +import org.json.JSONException; + + +/** + * A service for the ProtoLog logging system. + */ +public class ShellProtoLogImpl extends BaseProtoLogImpl { + private static final String TAG = "ProtoLogImpl"; + private static final int BUFFER_CAPACITY = 1024 * 1024; + // TODO: Get the right path for the proto log file when we initialize the shell components + private static final String LOG_FILENAME = new File("wm_shell_log.pb").getAbsolutePath(); + + private static ShellProtoLogImpl sServiceInstance = null; + + private final PrintWriter mSystemOutWriter; + + static { + addLogGroupEnum(ShellProtoLogGroup.values()); + } + + /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */ + public static void d(IProtoLogGroup group, int messageHash, int paramsMask, + @Nullable String messageString, + Object... args) { + getSingleInstance() + .log(LogLevel.DEBUG, group, messageHash, paramsMask, messageString, args); + } + + /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */ + public static void v(IProtoLogGroup group, int messageHash, int paramsMask, + @Nullable String messageString, + Object... args) { + getSingleInstance().log(LogLevel.VERBOSE, group, messageHash, paramsMask, messageString, + args); + } + + /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */ + public static void i(IProtoLogGroup group, int messageHash, int paramsMask, + @Nullable String messageString, + Object... args) { + getSingleInstance().log(LogLevel.INFO, group, messageHash, paramsMask, messageString, args); + } + + /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */ + public static void w(IProtoLogGroup group, int messageHash, int paramsMask, + @Nullable String messageString, + Object... args) { + getSingleInstance().log(LogLevel.WARN, group, messageHash, paramsMask, messageString, args); + } + + /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */ + public static void e(IProtoLogGroup group, int messageHash, int paramsMask, + @Nullable String messageString, + Object... args) { + getSingleInstance() + .log(LogLevel.ERROR, group, messageHash, paramsMask, messageString, args); + } + + /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */ + public static void wtf(IProtoLogGroup group, int messageHash, int paramsMask, + @Nullable String messageString, + Object... args) { + getSingleInstance().log(LogLevel.WTF, group, messageHash, paramsMask, messageString, args); + } + + /** Returns true iff logging is enabled for the given {@code IProtoLogGroup}. */ + public static boolean isEnabled(IProtoLogGroup group) { + return group.isLogToLogcat() + || (group.isLogToProto() && getSingleInstance().isProtoEnabled()); + } + + /** + * Returns the single instance of the ProtoLogImpl singleton class. + */ + public static synchronized ShellProtoLogImpl getSingleInstance() { + if (sServiceInstance == null) { + sServiceInstance = new ShellProtoLogImpl(); + } + return sServiceInstance; + } + + public void startTextLogging(Context context, String... groups) { + try { + mViewerConfig.loadViewerConfig( + context.getResources().openRawResource(R.raw.wm_shell_protolog)); + setLogging(true /* setTextLogging */, true, mSystemOutWriter, groups); + } catch (IOException e) { + Log.i(TAG, "Unable to load log definitions: IOException while reading " + + "wm_shell_protolog. " + e); + } catch (JSONException e) { + Log.i(TAG, "Unable to load log definitions: JSON parsing exception while reading " + + "wm_shell_protolog. " + e); + } + } + + public void stopTextLogging(String... groups) { + setLogging(true /* setTextLogging */, false, mSystemOutWriter, groups); + } + + private ShellProtoLogImpl() { + super(new File(LOG_FILENAME), null, BUFFER_CAPACITY, + new ProtoLogViewerConfigReader()); + mSystemOutWriter = new PrintWriter(System.out, true); + } +} + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerHandleView.java new file mode 100644 index 000000000000..2cb1fff4cde6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerHandleView.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.splitscreen; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.util.Property; +import android.view.View; + +import com.android.wm.shell.R; +import com.android.wm.shell.animation.Interpolators; + +/** + * View for the handle in the docked stack divider. + */ +public class DividerHandleView extends View { + + private static final Property<DividerHandleView, Integer> WIDTH_PROPERTY = + new Property<DividerHandleView, Integer>(Integer.class, "width") { + @Override + public Integer get(DividerHandleView object) { + return object.mCurrentWidth; + } + + @Override + public void set(DividerHandleView object, Integer value) { + object.mCurrentWidth = value; + object.invalidate(); + } + }; + + private static final Property<DividerHandleView, Integer> HEIGHT_PROPERTY = + new Property<DividerHandleView, Integer>(Integer.class, "height") { + @Override + public Integer get(DividerHandleView object) { + return object.mCurrentHeight; + } + + @Override + public void set(DividerHandleView object, Integer value) { + object.mCurrentHeight = value; + object.invalidate(); + } + }; + + private final Paint mPaint = new Paint(); + private final int mWidth; + private final int mHeight; + private final int mCircleDiameter; + private int mCurrentWidth; + private int mCurrentHeight; + private AnimatorSet mAnimator; + private boolean mTouching; + + public DividerHandleView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + mPaint.setColor(getResources().getColor(R.color.docked_divider_handle, null)); + mPaint.setAntiAlias(true); + mWidth = getResources().getDimensionPixelSize(R.dimen.docked_divider_handle_width); + mHeight = getResources().getDimensionPixelSize(R.dimen.docked_divider_handle_height); + mCurrentWidth = mWidth; + mCurrentHeight = mHeight; + mCircleDiameter = (mWidth + mHeight) / 3; + } + + void setTouching(boolean touching, boolean animate) { + if (touching == mTouching) { + return; + } + if (mAnimator != null) { + mAnimator.cancel(); + mAnimator = null; + } + if (!animate) { + if (touching) { + mCurrentWidth = mCircleDiameter; + mCurrentHeight = mCircleDiameter; + } else { + mCurrentWidth = mWidth; + mCurrentHeight = mHeight; + } + invalidate(); + } else { + animateToTarget(touching ? mCircleDiameter : mWidth, + touching ? mCircleDiameter : mHeight, touching); + } + mTouching = touching; + } + + private void animateToTarget(int targetWidth, int targetHeight, boolean touching) { + ObjectAnimator widthAnimator = ObjectAnimator.ofInt(this, WIDTH_PROPERTY, + mCurrentWidth, targetWidth); + ObjectAnimator heightAnimator = ObjectAnimator.ofInt(this, HEIGHT_PROPERTY, + mCurrentHeight, targetHeight); + mAnimator = new AnimatorSet(); + mAnimator.playTogether(widthAnimator, heightAnimator); + mAnimator.setDuration(touching + ? DividerView.TOUCH_ANIMATION_DURATION + : DividerView.TOUCH_RELEASE_ANIMATION_DURATION); + mAnimator.setInterpolator(touching + ? Interpolators.TOUCH_RESPONSE + : Interpolators.FAST_OUT_SLOW_IN); + mAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mAnimator = null; + } + }); + mAnimator.start(); + } + + @Override + protected void onDraw(Canvas canvas) { + int left = getWidth() / 2 - mCurrentWidth / 2; + int top = getHeight() / 2 - mCurrentHeight / 2; + int radius = Math.min(mCurrentWidth, mCurrentHeight) / 2; + canvas.drawRoundRect(left, top, left + mCurrentWidth, top + mCurrentHeight, + radius, radius, mPaint); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerImeController.java new file mode 100644 index 000000000000..ff617ed466d1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerImeController.java @@ -0,0 +1,415 @@ +/* + * 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.wm.shell.splitscreen; + +import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED; +import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.Nullable; +import android.graphics.Rect; +import android.os.Handler; +import android.util.Slog; +import android.view.SurfaceControl; +import android.window.TaskOrganizer; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.TransactionPool; + +class DividerImeController implements DisplayImeController.ImePositionProcessor { + private static final String TAG = "DividerImeController"; + private static final boolean DEBUG = SplitScreenController.DEBUG; + + private static final float ADJUSTED_NONFOCUS_DIM = 0.3f; + + private final SplitScreenTaskOrganizer mSplits; + private final TransactionPool mTransactionPool; + private final Handler mHandler; + private final TaskOrganizer mTaskOrganizer; + + /** + * These are the y positions of the top of the IME surface when it is hidden and when it is + * shown respectively. These are NOT necessarily the top of the visible IME itself. + */ + private int mHiddenTop = 0; + private int mShownTop = 0; + + // The following are target states (what we are curretly animating towards). + /** + * {@code true} if, at the end of the animation, the split task positions should be + * adjusted by height of the IME. This happens when the secondary split is the IME target. + */ + private boolean mTargetAdjusted = false; + /** + * {@code true} if, at the end of the animation, the IME should be shown/visible + * regardless of what has focus. + */ + private boolean mTargetShown = false; + private float mTargetPrimaryDim = 0.f; + private float mTargetSecondaryDim = 0.f; + + // The following are the current (most recent) states set during animation + /** {@code true} if the secondary split has IME focus. */ + private boolean mSecondaryHasFocus = false; + /** The dimming currently applied to the primary/secondary splits. */ + private float mLastPrimaryDim = 0.f; + private float mLastSecondaryDim = 0.f; + /** The most recent y position of the top of the IME surface */ + private int mLastAdjustTop = -1; + + // The following are states reached last time an animation fully completed. + /** {@code true} if the IME was shown/visible by the last-completed animation. */ + private boolean mImeWasShown = false; + /** {@code true} if the split positions were adjusted by the last-completed animation. */ + private boolean mAdjusted = false; + + /** + * When some aspect of split-screen needs to animate independent from the IME, + * this will be non-null and control split animation. + */ + @Nullable + private ValueAnimator mAnimation = null; + + private boolean mPaused = true; + private boolean mPausedTargetAdjusted = false; + private boolean mAdjustedWhileHidden = false; + + DividerImeController(SplitScreenTaskOrganizer splits, TransactionPool pool, Handler handler, + TaskOrganizer taskOrganizer) { + mSplits = splits; + mTransactionPool = pool; + mHandler = handler; + mTaskOrganizer = taskOrganizer; + } + + private DividerView getView() { + return mSplits.mSplitScreenController.getDividerView(); + } + + private SplitDisplayLayout getLayout() { + return mSplits.mSplitScreenController.getSplitLayout(); + } + + private boolean isDividerVisible() { + return mSplits.mSplitScreenController.isDividerVisible(); + } + + private boolean getSecondaryHasFocus(int displayId) { + WindowContainerToken imeSplit = mTaskOrganizer.getImeTarget(displayId); + return imeSplit != null + && (imeSplit.asBinder() == mSplits.mSecondary.token.asBinder()); + } + + void reset() { + mPaused = true; + mPausedTargetAdjusted = false; + mAdjustedWhileHidden = false; + mAnimation = null; + mAdjusted = mTargetAdjusted = false; + mImeWasShown = mTargetShown = false; + mTargetPrimaryDim = mTargetSecondaryDim = mLastPrimaryDim = mLastSecondaryDim = 0.f; + mSecondaryHasFocus = false; + mLastAdjustTop = -1; + } + + private void updateDimTargets() { + final boolean splitIsVisible = !getView().isHidden(); + mTargetPrimaryDim = (mSecondaryHasFocus && mTargetShown && splitIsVisible) + ? ADJUSTED_NONFOCUS_DIM : 0.f; + mTargetSecondaryDim = (!mSecondaryHasFocus && mTargetShown && splitIsVisible) + ? ADJUSTED_NONFOCUS_DIM : 0.f; + } + + @Override + @ImeAnimationFlags + public int onImeStartPositioning(int displayId, int hiddenTop, int shownTop, + boolean imeShouldShow, boolean imeIsFloating, SurfaceControl.Transaction t) { + mHiddenTop = hiddenTop; + mShownTop = shownTop; + mTargetShown = imeShouldShow; + if (!isDividerVisible()) { + return 0; + } + final boolean splitIsVisible = !getView().isHidden(); + mSecondaryHasFocus = getSecondaryHasFocus(displayId); + final boolean targetAdjusted = splitIsVisible && imeShouldShow && mSecondaryHasFocus + && !imeIsFloating && !getLayout().mDisplayLayout.isLandscape() + && !mSplits.mSplitScreenController.isMinimized(); + if (mLastAdjustTop < 0) { + mLastAdjustTop = imeShouldShow ? hiddenTop : shownTop; + } else if (mLastAdjustTop != (imeShouldShow ? mShownTop : mHiddenTop)) { + if (mTargetAdjusted != targetAdjusted && targetAdjusted == mAdjusted) { + // Check for an "interruption" of an existing animation. In this case, we + // need to fake-flip the last-known state direction so that the animation + // completes in the other direction. + mAdjusted = mTargetAdjusted; + } else if (targetAdjusted && mTargetAdjusted && mAdjusted) { + // Already fully adjusted for IME, but IME height has changed; so, force-start + // an async animation to the new IME height. + mAdjusted = false; + } + } + if (mPaused) { + mPausedTargetAdjusted = targetAdjusted; + if (DEBUG) Slog.d(TAG, " ime starting but paused " + dumpState()); + return (targetAdjusted || mAdjusted) ? IME_ANIMATION_NO_ALPHA : 0; + } + mTargetAdjusted = targetAdjusted; + updateDimTargets(); + if (DEBUG) Slog.d(TAG, " ime starting. vis:" + splitIsVisible + " " + dumpState()); + if (mAnimation != null || (mImeWasShown && imeShouldShow + && mTargetAdjusted != mAdjusted)) { + // We need to animate adjustment independently of the IME position, so + // start our own animation to drive adjustment. This happens when a + // different split's editor has gained focus while the IME is still visible. + startAsyncAnimation(); + } + if (splitIsVisible) { + // If split is hidden, we don't want to trigger any relayouts that would cause the + // divider to show again. + updateImeAdjustState(); + } else { + mAdjustedWhileHidden = true; + } + return (mTargetAdjusted || mAdjusted) ? IME_ANIMATION_NO_ALPHA : 0; + } + + private void updateImeAdjustState() { + updateImeAdjustState(false /* force */); + } + + private void updateImeAdjustState(boolean force) { + if (mAdjusted != mTargetAdjusted || force) { + // Reposition the server's secondary split position so that it evaluates + // insets properly. + WindowContainerTransaction wct = new WindowContainerTransaction(); + final SplitDisplayLayout splitLayout = getLayout(); + if (mTargetAdjusted) { + splitLayout.updateAdjustedBounds(mShownTop, mHiddenTop, mShownTop); + wct.setBounds(mSplits.mSecondary.token, splitLayout.mAdjustedSecondary); + // "Freeze" the configuration size so that the app doesn't get a config + // or relaunch. This is required because normally nav-bar contributes + // to configuration bounds (via nondecorframe). + Rect adjustAppBounds = new Rect(mSplits.mSecondary.configuration + .windowConfiguration.getAppBounds()); + adjustAppBounds.offset(0, splitLayout.mAdjustedSecondary.top + - splitLayout.mSecondary.top); + wct.setAppBounds(mSplits.mSecondary.token, adjustAppBounds); + wct.setScreenSizeDp(mSplits.mSecondary.token, + mSplits.mSecondary.configuration.screenWidthDp, + mSplits.mSecondary.configuration.screenHeightDp); + + wct.setBounds(mSplits.mPrimary.token, splitLayout.mAdjustedPrimary); + adjustAppBounds = new Rect(mSplits.mPrimary.configuration + .windowConfiguration.getAppBounds()); + adjustAppBounds.offset(0, splitLayout.mAdjustedPrimary.top + - splitLayout.mPrimary.top); + wct.setAppBounds(mSplits.mPrimary.token, adjustAppBounds); + wct.setScreenSizeDp(mSplits.mPrimary.token, + mSplits.mPrimary.configuration.screenWidthDp, + mSplits.mPrimary.configuration.screenHeightDp); + } else { + wct.setBounds(mSplits.mSecondary.token, splitLayout.mSecondary); + wct.setAppBounds(mSplits.mSecondary.token, null); + wct.setScreenSizeDp(mSplits.mSecondary.token, + SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); + wct.setBounds(mSplits.mPrimary.token, splitLayout.mPrimary); + wct.setAppBounds(mSplits.mPrimary.token, null); + wct.setScreenSizeDp(mSplits.mPrimary.token, + SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); + } + + if (!mSplits.mSplitScreenController.getWmProxy().queueSyncTransactionIfWaiting(wct)) { + mTaskOrganizer.applyTransaction(wct); + } + } + + // Update all the adjusted-for-ime states + if (!mPaused) { + final DividerView view = getView(); + if (view != null) { + view.setAdjustedForIme(mTargetShown, mTargetShown + ? DisplayImeController.ANIMATION_DURATION_SHOW_MS + : DisplayImeController.ANIMATION_DURATION_HIDE_MS); + } + } + mSplits.mSplitScreenController.setAdjustedForIme(mTargetShown && !mPaused); + } + + public void updateAdjustForIme() { + updateImeAdjustState(mAdjustedWhileHidden); + mAdjustedWhileHidden = false; + } + + @Override + public void onImePositionChanged(int displayId, int imeTop, + SurfaceControl.Transaction t) { + if (mAnimation != null || !isDividerVisible() || mPaused) { + // Not synchronized with IME anymore, so return. + return; + } + final float fraction = ((float) imeTop - mHiddenTop) / (mShownTop - mHiddenTop); + final float progress = mTargetShown ? fraction : 1.f - fraction; + onProgress(progress, t); + } + + @Override + public void onImeEndPositioning(int displayId, boolean cancelled, + SurfaceControl.Transaction t) { + if (mAnimation != null || !isDividerVisible() || mPaused) { + // Not synchronized with IME anymore, so return. + return; + } + onEnd(cancelled, t); + } + + private void onProgress(float progress, SurfaceControl.Transaction t) { + final DividerView view = getView(); + if (mTargetAdjusted != mAdjusted && !mPaused) { + final SplitDisplayLayout splitLayout = getLayout(); + final float fraction = mTargetAdjusted ? progress : 1.f - progress; + mLastAdjustTop = (int) (fraction * mShownTop + (1.f - fraction) * mHiddenTop); + splitLayout.updateAdjustedBounds(mLastAdjustTop, mHiddenTop, mShownTop); + view.resizeSplitSurfaces(t, splitLayout.mAdjustedPrimary, + splitLayout.mAdjustedSecondary); + } + final float invProg = 1.f - progress; + view.setResizeDimLayer(t, true /* primary */, + mLastPrimaryDim * invProg + progress * mTargetPrimaryDim); + view.setResizeDimLayer(t, false /* primary */, + mLastSecondaryDim * invProg + progress * mTargetSecondaryDim); + } + + void setDimsHidden(SurfaceControl.Transaction t, boolean hidden) { + final DividerView view = getView(); + if (hidden) { + view.setResizeDimLayer(t, true /* primary */, 0.f /* alpha */); + view.setResizeDimLayer(t, false /* primary */, 0.f /* alpha */); + } else { + updateDimTargets(); + view.setResizeDimLayer(t, true /* primary */, mTargetPrimaryDim); + view.setResizeDimLayer(t, false /* primary */, mTargetSecondaryDim); + } + } + + private void onEnd(boolean cancelled, SurfaceControl.Transaction t) { + if (!cancelled) { + onProgress(1.f, t); + mAdjusted = mTargetAdjusted; + mImeWasShown = mTargetShown; + mLastAdjustTop = mAdjusted ? mShownTop : mHiddenTop; + mLastPrimaryDim = mTargetPrimaryDim; + mLastSecondaryDim = mTargetSecondaryDim; + } + } + + private void startAsyncAnimation() { + if (mAnimation != null) { + mAnimation.cancel(); + } + mAnimation = ValueAnimator.ofFloat(0.f, 1.f); + mAnimation.setDuration(DisplayImeController.ANIMATION_DURATION_SHOW_MS); + if (mTargetAdjusted != mAdjusted) { + final float fraction = + ((float) mLastAdjustTop - mHiddenTop) / (mShownTop - mHiddenTop); + final float progress = mTargetAdjusted ? fraction : 1.f - fraction; + mAnimation.setCurrentFraction(progress); + } + + mAnimation.addUpdateListener(animation -> { + SurfaceControl.Transaction t = mTransactionPool.acquire(); + float value = (float) animation.getAnimatedValue(); + onProgress(value, t); + t.apply(); + mTransactionPool.release(t); + }); + mAnimation.setInterpolator(DisplayImeController.INTERPOLATOR); + mAnimation.addListener(new AnimatorListenerAdapter() { + private boolean mCancel = false; + + @Override + public void onAnimationCancel(Animator animation) { + mCancel = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + SurfaceControl.Transaction t = mTransactionPool.acquire(); + onEnd(mCancel, t); + t.apply(); + mTransactionPool.release(t); + mAnimation = null; + } + }); + mAnimation.start(); + } + + private String dumpState() { + return "top:" + mHiddenTop + "->" + mShownTop + + " adj:" + mAdjusted + "->" + mTargetAdjusted + "(" + mLastAdjustTop + ")" + + " shw:" + mImeWasShown + "->" + mTargetShown + + " dims:" + mLastPrimaryDim + "," + mLastSecondaryDim + + "->" + mTargetPrimaryDim + "," + mTargetSecondaryDim + + " shf:" + mSecondaryHasFocus + " desync:" + (mAnimation != null) + + " paus:" + mPaused + "[" + mPausedTargetAdjusted + "]"; + } + + /** Completely aborts/resets adjustment state */ + public void pause(int displayId) { + if (DEBUG) Slog.d(TAG, "ime pause posting " + dumpState()); + mHandler.post(() -> { + if (DEBUG) Slog.d(TAG, "ime pause run posted " + dumpState()); + if (mPaused) { + return; + } + mPaused = true; + mPausedTargetAdjusted = mTargetAdjusted; + mTargetAdjusted = false; + mTargetPrimaryDim = mTargetSecondaryDim = 0.f; + updateImeAdjustState(); + startAsyncAnimation(); + if (mAnimation != null) { + mAnimation.end(); + } + }); + } + + public void resume(int displayId) { + if (DEBUG) Slog.d(TAG, "ime resume posting " + dumpState()); + mHandler.post(() -> { + if (DEBUG) Slog.d(TAG, "ime resume run posted " + dumpState()); + if (!mPaused) { + return; + } + mPaused = false; + mTargetAdjusted = mPausedTargetAdjusted; + updateDimTargets(); + final DividerView view = getView(); + if ((mTargetAdjusted != mAdjusted) && !mSplits.mSplitScreenController.isMinimized() + && view != null) { + // End unminimize animations since they conflict with adjustment animations. + view.finishAnimations(); + } + updateImeAdjustState(); + startAsyncAnimation(); + }); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerState.java new file mode 100644 index 000000000000..23d86a00d4bf --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerState.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.splitscreen; + +/** + * Class to hold state of divider that needs to persist across configuration changes. + */ +final class DividerState { + public boolean animateAfterRecentsDrawn; + public float mRatioPositionBeforeMinimized; +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerView.java new file mode 100644 index 000000000000..00146e9447bd --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerView.java @@ -0,0 +1,1374 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.splitscreen; + +import static android.view.PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW; +import static android.view.PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; +import static android.view.WindowManager.DOCKED_RIGHT; + +import android.animation.AnimationHandler; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.Region.Op; +import android.hardware.display.DisplayManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.RemoteException; +import android.util.AttributeSet; +import android.util.Slog; +import android.view.Display; +import android.view.MotionEvent; +import android.view.PointerIcon; +import android.view.SurfaceControl; +import android.view.SurfaceControl.Transaction; +import android.view.VelocityTracker; +import android.view.View; +import android.view.View.OnTouchListener; +import android.view.ViewConfiguration; +import android.view.ViewRootImpl; +import android.view.ViewTreeObserver.InternalInsetsInfo; +import android.view.ViewTreeObserver.OnComputeInternalInsetsListener; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; +import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; +import android.widget.FrameLayout; + +import com.android.internal.graphics.SfVsyncFrameCallbackProvider; +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.policy.DividerSnapAlgorithm; +import com.android.internal.policy.DividerSnapAlgorithm.SnapTarget; +import com.android.internal.policy.DockedDividerUtils; +import com.android.wm.shell.R; +import com.android.wm.shell.animation.FlingAnimationUtils; +import com.android.wm.shell.animation.Interpolators; + +import java.util.function.Consumer; + +/** + * Docked stack divider. + */ +public class DividerView extends FrameLayout implements OnTouchListener, + OnComputeInternalInsetsListener { + private static final String TAG = "DividerView"; + private static final boolean DEBUG = SplitScreenController.DEBUG; + + interface DividerCallbacks { + void onDraggingStart(); + void onDraggingEnd(); + } + + static final long TOUCH_ANIMATION_DURATION = 150; + static final long TOUCH_RELEASE_ANIMATION_DURATION = 200; + + public static final int INVALID_RECENTS_GROW_TARGET = -1; + + private static final int LOG_VALUE_RESIZE_50_50 = 0; + private static final int LOG_VALUE_RESIZE_DOCKED_SMALLER = 1; + private static final int LOG_VALUE_RESIZE_DOCKED_LARGER = 2; + + private static final int LOG_VALUE_UNDOCK_MAX_DOCKED = 0; + private static final int LOG_VALUE_UNDOCK_MAX_OTHER = 1; + + private static final int TASK_POSITION_SAME = Integer.MAX_VALUE; + + /** + * How much the background gets scaled when we are in the minimized dock state. + */ + private static final float MINIMIZE_DOCK_SCALE = 0f; + private static final float ADJUSTED_FOR_IME_SCALE = 0.5f; + + private static final PathInterpolator SLOWDOWN_INTERPOLATOR = + new PathInterpolator(0.5f, 1f, 0.5f, 1f); + private static final PathInterpolator DIM_INTERPOLATOR = + new PathInterpolator(.23f, .87f, .52f, -0.11f); + private static final Interpolator IME_ADJUST_INTERPOLATOR = + new PathInterpolator(0.2f, 0f, 0.1f, 1f); + + private DividerHandleView mHandle; + private View mBackground; + private MinimizedDockShadow mMinimizedShadow; + private int mStartX; + private int mStartY; + private int mStartPosition; + private int mDockSide; + private boolean mMoving; + private int mTouchSlop; + private boolean mBackgroundLifted; + private boolean mIsInMinimizeInteraction; + SnapTarget mSnapTargetBeforeMinimized; + + private int mDividerInsets; + private final Display mDefaultDisplay; + + private int mDividerSize; + private int mTouchElevation; + private int mLongPressEntraceAnimDuration; + + private final Rect mDockedRect = new Rect(); + private final Rect mDockedTaskRect = new Rect(); + private final Rect mOtherTaskRect = new Rect(); + private final Rect mOtherRect = new Rect(); + private final Rect mDockedInsetRect = new Rect(); + private final Rect mOtherInsetRect = new Rect(); + private final Rect mLastResizeRect = new Rect(); + private final Rect mTmpRect = new Rect(); + private SplitScreenController mSplitScreenController; + private WindowManagerProxy mWindowManagerProxy; + private DividerWindowManager mWindowManager; + private VelocityTracker mVelocityTracker; + private FlingAnimationUtils mFlingAnimationUtils; + private SplitDisplayLayout mSplitLayout; + private DividerImeController mImeController; + private DividerCallbacks mCallback; + private final AnimationHandler mAnimationHandler = new AnimationHandler(); + + private ValueAnimator mCurrentAnimator; + private boolean mEntranceAnimationRunning; + private boolean mExitAnimationRunning; + private int mExitStartPosition; + private boolean mDockedStackMinimized; + private boolean mHomeStackResizable; + private boolean mAdjustedForIme; + private DividerState mState; + + private SplitScreenTaskOrganizer mTiles; + boolean mFirstLayout = true; + int mDividerPositionX; + int mDividerPositionY; + + private final Matrix mTmpMatrix = new Matrix(); + private final float[] mTmpValues = new float[9]; + + // The view is removed or in the process of been removed from the system. + private boolean mRemoved; + + // Whether the surface for this view has been hidden regardless of actual visibility. This is + // used interact with keyguard. + private boolean mSurfaceHidden = false; + + private final Handler mHandler = new Handler(); + + private final AccessibilityDelegate mHandleDelegate = new AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + final DividerSnapAlgorithm snapAlgorithm = getSnapAlgorithm(); + if (isHorizontalDivision()) { + info.addAction(new AccessibilityAction(R.id.action_move_tl_full, + mContext.getString(R.string.accessibility_action_divider_top_full))); + if (snapAlgorithm.isFirstSplitTargetAvailable()) { + info.addAction(new AccessibilityAction(R.id.action_move_tl_70, + mContext.getString(R.string.accessibility_action_divider_top_70))); + } + if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) { + // Only show the middle target if there are more than 1 split target + info.addAction(new AccessibilityAction(R.id.action_move_tl_50, + mContext.getString(R.string.accessibility_action_divider_top_50))); + } + if (snapAlgorithm.isLastSplitTargetAvailable()) { + info.addAction(new AccessibilityAction(R.id.action_move_tl_30, + mContext.getString(R.string.accessibility_action_divider_top_30))); + } + info.addAction(new AccessibilityAction(R.id.action_move_rb_full, + mContext.getString(R.string.accessibility_action_divider_bottom_full))); + } else { + info.addAction(new AccessibilityAction(R.id.action_move_tl_full, + mContext.getString(R.string.accessibility_action_divider_left_full))); + if (snapAlgorithm.isFirstSplitTargetAvailable()) { + info.addAction(new AccessibilityAction(R.id.action_move_tl_70, + mContext.getString(R.string.accessibility_action_divider_left_70))); + } + if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) { + // Only show the middle target if there are more than 1 split target + info.addAction(new AccessibilityAction(R.id.action_move_tl_50, + mContext.getString(R.string.accessibility_action_divider_left_50))); + } + if (snapAlgorithm.isLastSplitTargetAvailable()) { + info.addAction(new AccessibilityAction(R.id.action_move_tl_30, + mContext.getString(R.string.accessibility_action_divider_left_30))); + } + info.addAction(new AccessibilityAction(R.id.action_move_rb_full, + mContext.getString(R.string.accessibility_action_divider_right_full))); + } + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + int currentPosition = getCurrentPosition(); + SnapTarget nextTarget = null; + DividerSnapAlgorithm snapAlgorithm = mSplitLayout.getSnapAlgorithm(); + if (action == R.id.action_move_tl_full) { + nextTarget = snapAlgorithm.getDismissEndTarget(); + } else if (action == R.id.action_move_tl_70) { + nextTarget = snapAlgorithm.getLastSplitTarget(); + } else if (action == R.id.action_move_tl_50) { + nextTarget = snapAlgorithm.getMiddleTarget(); + } else if (action == R.id.action_move_tl_30) { + nextTarget = snapAlgorithm.getFirstSplitTarget(); + } else if (action == R.id.action_move_rb_full) { + nextTarget = snapAlgorithm.getDismissStartTarget(); + } + if (nextTarget != null) { + startDragging(true /* animate */, false /* touching */); + stopDragging(currentPosition, nextTarget, 250, Interpolators.FAST_OUT_SLOW_IN); + return true; + } + return super.performAccessibilityAction(host, action, args); + } + }; + + private final Runnable mResetBackgroundRunnable = new Runnable() { + @Override + public void run() { + resetBackground(); + } + }; + + private Runnable mUpdateEmbeddedMatrix = () -> { + if (getViewRootImpl() == null) { + return; + } + if (isHorizontalDivision()) { + mTmpMatrix.setTranslate(0, mDividerPositionY - mDividerInsets); + } else { + mTmpMatrix.setTranslate(mDividerPositionX - mDividerInsets, 0); + } + mTmpMatrix.getValues(mTmpValues); + try { + getViewRootImpl().getAccessibilityEmbeddedConnection().setScreenMatrix(mTmpValues); + } catch (RemoteException e) { + } + }; + + public DividerView(Context context) { + this(context, null); + } + + public DividerView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public DividerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public DividerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + final DisplayManager displayManager = + (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE); + mDefaultDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY); + mAnimationHandler.setProvider(new SfVsyncFrameCallbackProvider()); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mHandle = findViewById(R.id.docked_divider_handle); + mBackground = findViewById(R.id.docked_divider_background); + mMinimizedShadow = findViewById(R.id.minimized_dock_shadow); + mHandle.setOnTouchListener(this); + final int dividerWindowWidth = getResources().getDimensionPixelSize( + com.android.internal.R.dimen.docked_stack_divider_thickness); + mDividerInsets = getResources().getDimensionPixelSize( + com.android.internal.R.dimen.docked_stack_divider_insets); + mDividerSize = dividerWindowWidth - 2 * mDividerInsets; + mTouchElevation = getResources().getDimensionPixelSize( + R.dimen.docked_stack_divider_lift_elevation); + mLongPressEntraceAnimDuration = getResources().getInteger( + R.integer.long_press_dock_anim_duration); + mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); + mFlingAnimationUtils = new FlingAnimationUtils(getResources().getDisplayMetrics(), 0.3f); + boolean landscape = getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE; + mHandle.setPointerIcon(PointerIcon.getSystemIcon(getContext(), + landscape ? TYPE_HORIZONTAL_DOUBLE_ARROW : TYPE_VERTICAL_DOUBLE_ARROW)); + getViewTreeObserver().addOnComputeInternalInsetsListener(this); + mHandle.setAccessibilityDelegate(mHandleDelegate); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + // Save the current target if not minimized once attached to window + if (mDockSide != WindowManager.DOCKED_INVALID && !mIsInMinimizeInteraction) { + saveSnapTargetBeforeMinimized(mSnapTargetBeforeMinimized); + } + mFirstLayout = true; + } + + void onDividerRemoved() { + mRemoved = true; + mCallback = null; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (mFirstLayout) { + // Wait for first layout so that the ViewRootImpl surface has been created. + initializeSurfaceState(); + mFirstLayout = false; + } + int minimizeLeft = 0; + int minimizeTop = 0; + if (mDockSide == WindowManager.DOCKED_TOP) { + minimizeTop = mBackground.getTop(); + } else if (mDockSide == WindowManager.DOCKED_LEFT) { + minimizeLeft = mBackground.getLeft(); + } else if (mDockSide == WindowManager.DOCKED_RIGHT) { + minimizeLeft = mBackground.getRight() - mMinimizedShadow.getWidth(); + } + mMinimizedShadow.layout(minimizeLeft, minimizeTop, + minimizeLeft + mMinimizedShadow.getMeasuredWidth(), + minimizeTop + mMinimizedShadow.getMeasuredHeight()); + if (changed) { + notifySplitScreenBoundsChanged(); + } + } + + void injectDependencies(SplitScreenController splitScreenController, + DividerWindowManager windowManager, DividerState dividerState, + DividerCallbacks callback, SplitScreenTaskOrganizer tiles, SplitDisplayLayout sdl, + DividerImeController imeController, WindowManagerProxy wmProxy) { + mSplitScreenController = splitScreenController; + mWindowManager = windowManager; + mState = dividerState; + mCallback = callback; + mTiles = tiles; + mSplitLayout = sdl; + mImeController = imeController; + mWindowManagerProxy = wmProxy; + + if (mState.mRatioPositionBeforeMinimized == 0) { + // Set the middle target as the initial state + mSnapTargetBeforeMinimized = mSplitLayout.getSnapAlgorithm().getMiddleTarget(); + } else { + repositionSnapTargetBeforeMinimized(); + } + } + + /** Gets non-minimized secondary bounds of split screen. */ + public Rect getNonMinimizedSplitScreenSecondaryBounds() { + mOtherTaskRect.set(mSplitLayout.mSecondary); + return mOtherTaskRect; + } + + private boolean inSplitMode() { + return getVisibility() == VISIBLE; + } + + /** Unlike setVisible, this directly hides the surface without changing view visibility. */ + void setHidden(boolean hidden) { + if (mSurfaceHidden == hidden) { + return; + } + mSurfaceHidden = hidden; + post(() -> { + final SurfaceControl sc = getWindowSurfaceControl(); + if (sc == null) { + return; + } + Transaction t = mTiles.getTransaction(); + if (hidden) { + t.hide(sc); + } else { + t.show(sc); + } + mImeController.setDimsHidden(t, hidden); + t.apply(); + mTiles.releaseTransaction(t); + }); + } + + boolean isHidden() { + return mSurfaceHidden; + } + + /** Starts dragging the divider bar. */ + public boolean startDragging(boolean animate, boolean touching) { + cancelFlingAnimation(); + if (touching) { + mHandle.setTouching(true, animate); + } + mDockSide = mSplitLayout.getPrimarySplitSide(); + + mWindowManagerProxy.setResizing(true); + if (touching) { + mWindowManager.setSlippery(false); + liftBackground(); + } + if (mCallback != null) { + mCallback.onDraggingStart(); + } + return inSplitMode(); + } + + /** Stops dragging the divider bar. */ + public void stopDragging(int position, float velocity, boolean avoidDismissStart, + boolean logMetrics) { + mHandle.setTouching(false, true /* animate */); + fling(position, velocity, avoidDismissStart, logMetrics); + mWindowManager.setSlippery(true); + releaseBackground(); + } + + private void stopDragging(int position, SnapTarget target, long duration, + Interpolator interpolator) { + stopDragging(position, target, duration, 0 /* startDelay*/, 0 /* endDelay */, interpolator); + } + + private void stopDragging(int position, SnapTarget target, long duration, + Interpolator interpolator, long endDelay) { + stopDragging(position, target, duration, 0 /* startDelay*/, endDelay, interpolator); + } + + private void stopDragging(int position, SnapTarget target, long duration, long startDelay, + long endDelay, Interpolator interpolator) { + mHandle.setTouching(false, true /* animate */); + flingTo(position, target, duration, startDelay, endDelay, interpolator); + mWindowManager.setSlippery(true); + releaseBackground(); + } + + private void stopDragging() { + mHandle.setTouching(false, true /* animate */); + mWindowManager.setSlippery(true); + releaseBackground(); + } + + private void updateDockSide() { + mDockSide = mSplitLayout.getPrimarySplitSide(); + mMinimizedShadow.setDockSide(mDockSide); + } + + public DividerSnapAlgorithm getSnapAlgorithm() { + return mDockedStackMinimized ? mSplitLayout.getMinimizedSnapAlgorithm(mHomeStackResizable) + : mSplitLayout.getSnapAlgorithm(); + } + + public int getCurrentPosition() { + return isHorizontalDivision() ? mDividerPositionY : mDividerPositionX; + } + + public boolean isMinimized() { + return mDockedStackMinimized; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + convertToScreenCoordinates(event); + final int action = event.getAction() & MotionEvent.ACTION_MASK; + switch (action) { + case MotionEvent.ACTION_DOWN: + mVelocityTracker = VelocityTracker.obtain(); + mVelocityTracker.addMovement(event); + mStartX = (int) event.getX(); + mStartY = (int) event.getY(); + boolean result = startDragging(true /* animate */, true /* touching */); + if (!result) { + + // Weren't able to start dragging successfully, so cancel it again. + stopDragging(); + } + mStartPosition = getCurrentPosition(); + mMoving = false; + return result; + case MotionEvent.ACTION_MOVE: + mVelocityTracker.addMovement(event); + int x = (int) event.getX(); + int y = (int) event.getY(); + boolean exceededTouchSlop = + isHorizontalDivision() && Math.abs(y - mStartY) > mTouchSlop + || (!isHorizontalDivision() && Math.abs(x - mStartX) > mTouchSlop); + if (!mMoving && exceededTouchSlop) { + mStartX = x; + mStartY = y; + mMoving = true; + } + if (mMoving && mDockSide != WindowManager.DOCKED_INVALID) { + SnapTarget snapTarget = getSnapAlgorithm().calculateSnapTarget( + mStartPosition, 0 /* velocity */, false /* hardDismiss */); + resizeStackSurfaces(calculatePosition(x, y), mStartPosition, snapTarget, + null /* transaction */); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mVelocityTracker.addMovement(event); + + x = (int) event.getRawX(); + y = (int) event.getRawY(); + + mVelocityTracker.computeCurrentVelocity(1000); + int position = calculatePosition(x, y); + stopDragging(position, isHorizontalDivision() ? mVelocityTracker.getYVelocity() + : mVelocityTracker.getXVelocity(), false /* avoidDismissStart */, + true /* log */); + mMoving = false; + break; + } + return true; + } + + private void logResizeEvent(SnapTarget snapTarget) { + if (snapTarget == mSplitLayout.getSnapAlgorithm().getDismissStartTarget()) { + MetricsLogger.action( + mContext, MetricsEvent.ACTION_WINDOW_UNDOCK_MAX, dockSideTopLeft(mDockSide) + ? LOG_VALUE_UNDOCK_MAX_OTHER + : LOG_VALUE_UNDOCK_MAX_DOCKED); + } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getDismissEndTarget()) { + MetricsLogger.action( + mContext, MetricsEvent.ACTION_WINDOW_UNDOCK_MAX, dockSideBottomRight(mDockSide) + ? LOG_VALUE_UNDOCK_MAX_OTHER + : LOG_VALUE_UNDOCK_MAX_DOCKED); + } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getMiddleTarget()) { + MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_RESIZE, + LOG_VALUE_RESIZE_50_50); + } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getFirstSplitTarget()) { + MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_RESIZE, + dockSideTopLeft(mDockSide) + ? LOG_VALUE_RESIZE_DOCKED_SMALLER + : LOG_VALUE_RESIZE_DOCKED_LARGER); + } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getLastSplitTarget()) { + MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_RESIZE, + dockSideTopLeft(mDockSide) + ? LOG_VALUE_RESIZE_DOCKED_LARGER + : LOG_VALUE_RESIZE_DOCKED_SMALLER); + } + } + + private void convertToScreenCoordinates(MotionEvent event) { + event.setLocation(event.getRawX(), event.getRawY()); + } + + private void fling(int position, float velocity, boolean avoidDismissStart, + boolean logMetrics) { + DividerSnapAlgorithm currentSnapAlgorithm = getSnapAlgorithm(); + SnapTarget snapTarget = currentSnapAlgorithm.calculateSnapTarget(position, velocity); + if (avoidDismissStart && snapTarget == currentSnapAlgorithm.getDismissStartTarget()) { + snapTarget = currentSnapAlgorithm.getFirstSplitTarget(); + } + if (logMetrics) { + logResizeEvent(snapTarget); + } + ValueAnimator anim = getFlingAnimator(position, snapTarget, 0 /* endDelay */); + mFlingAnimationUtils.apply(anim, position, snapTarget.position, velocity); + anim.start(); + } + + private void flingTo(int position, SnapTarget target, long duration, long startDelay, + long endDelay, Interpolator interpolator) { + ValueAnimator anim = getFlingAnimator(position, target, endDelay); + anim.setDuration(duration); + anim.setStartDelay(startDelay); + anim.setInterpolator(interpolator); + anim.start(); + } + + private ValueAnimator getFlingAnimator(int position, final SnapTarget snapTarget, + final long endDelay) { + if (mCurrentAnimator != null) { + cancelFlingAnimation(); + updateDockSide(); + } + if (DEBUG) Slog.d(TAG, "Getting fling " + position + "->" + snapTarget.position); + final boolean taskPositionSameAtEnd = snapTarget.flag == SnapTarget.FLAG_NONE; + ValueAnimator anim = ValueAnimator.ofInt(position, snapTarget.position); + anim.addUpdateListener(animation -> resizeStackSurfaces((int) animation.getAnimatedValue(), + taskPositionSameAtEnd && animation.getAnimatedFraction() == 1f + ? TASK_POSITION_SAME + : snapTarget.taskPosition, + snapTarget, null /* transaction */)); + Consumer<Boolean> endAction = cancelled -> { + if (DEBUG) Slog.d(TAG, "End Fling " + cancelled + " min:" + mIsInMinimizeInteraction); + final boolean wasMinimizeInteraction = mIsInMinimizeInteraction; + // Reset minimized divider position after unminimized state animation finishes. + if (!cancelled && !mDockedStackMinimized && mIsInMinimizeInteraction) { + mIsInMinimizeInteraction = false; + } + boolean dismissed = commitSnapFlags(snapTarget); + mWindowManagerProxy.setResizing(false); + updateDockSide(); + mCurrentAnimator = null; + mEntranceAnimationRunning = false; + mExitAnimationRunning = false; + if (!dismissed && !wasMinimizeInteraction) { + mWindowManagerProxy.applyResizeSplits(snapTarget.position, mSplitLayout); + } + if (mCallback != null) { + mCallback.onDraggingEnd(); + } + + // Record last snap target the divider moved to + if (!mIsInMinimizeInteraction) { + // The last snapTarget position can be negative when the last divider position was + // offscreen. In that case, save the middle (default) SnapTarget so calculating next + // position isn't negative. + final SnapTarget saveTarget; + if (snapTarget.position < 0) { + saveTarget = mSplitLayout.getSnapAlgorithm().getMiddleTarget(); + } else { + saveTarget = snapTarget; + } + final DividerSnapAlgorithm snapAlgo = mSplitLayout.getSnapAlgorithm(); + if (saveTarget.position != snapAlgo.getDismissEndTarget().position + && saveTarget.position != snapAlgo.getDismissStartTarget().position) { + saveSnapTargetBeforeMinimized(saveTarget); + } + } + notifySplitScreenBoundsChanged(); + }; + anim.addListener(new AnimatorListenerAdapter() { + + private boolean mCancelled; + + @Override + public void onAnimationCancel(Animator animation) { + mCancelled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + long delay = 0; + if (endDelay != 0) { + delay = endDelay; + } else if (mCancelled) { + delay = 0; + } + if (delay == 0) { + endAction.accept(mCancelled); + } else { + final Boolean cancelled = mCancelled; + if (DEBUG) Slog.d(TAG, "Posting endFling " + cancelled + " d:" + delay + "ms"); + mHandler.postDelayed(() -> endAction.accept(cancelled), delay); + } + } + }); + anim.setAnimationHandler(mAnimationHandler); + mCurrentAnimator = anim; + return anim; + } + + private void notifySplitScreenBoundsChanged() { + if (mSplitLayout.mPrimary == null || mSplitLayout.mSecondary == null) { + return; + } + mOtherTaskRect.set(mSplitLayout.mSecondary); + + mTmpRect.set(mHandle.getLeft(), mHandle.getTop(), mHandle.getRight(), mHandle.getBottom()); + if (isHorizontalDivision()) { + mTmpRect.offsetTo(0, mDividerPositionY); + } else { + mTmpRect.offsetTo(mDividerPositionX, 0); + } + mWindowManagerProxy.setTouchRegion(mTmpRect); + + mTmpRect.set(mSplitLayout.mDisplayLayout.stableInsets()); + switch (mSplitLayout.getPrimarySplitSide()) { + case WindowManager.DOCKED_LEFT: + mTmpRect.left = 0; + break; + case WindowManager.DOCKED_RIGHT: + mTmpRect.right = 0; + break; + case WindowManager.DOCKED_TOP: + mTmpRect.top = 0; + break; + } + mSplitScreenController.notifyBoundsChanged(mOtherTaskRect, mTmpRect); + } + + private void cancelFlingAnimation() { + if (mCurrentAnimator != null) { + mCurrentAnimator.cancel(); + } + } + + private boolean commitSnapFlags(SnapTarget target) { + if (target.flag == SnapTarget.FLAG_NONE) { + return false; + } + final boolean dismissOrMaximize; + if (target.flag == SnapTarget.FLAG_DISMISS_START) { + dismissOrMaximize = mDockSide == WindowManager.DOCKED_LEFT + || mDockSide == WindowManager.DOCKED_TOP; + } else { + dismissOrMaximize = mDockSide == WindowManager.DOCKED_RIGHT + || mDockSide == WindowManager.DOCKED_BOTTOM; + } + mWindowManagerProxy.dismissOrMaximizeDocked(mTiles, mSplitLayout, dismissOrMaximize); + Transaction t = mTiles.getTransaction(); + setResizeDimLayer(t, true /* primary */, 0f); + setResizeDimLayer(t, false /* primary */, 0f); + t.apply(); + mTiles.releaseTransaction(t); + return true; + } + + private void liftBackground() { + if (mBackgroundLifted) { + return; + } + if (isHorizontalDivision()) { + mBackground.animate().scaleY(1.4f); + } else { + mBackground.animate().scaleX(1.4f); + } + mBackground.animate() + .setInterpolator(Interpolators.TOUCH_RESPONSE) + .setDuration(TOUCH_ANIMATION_DURATION) + .translationZ(mTouchElevation) + .start(); + + // Lift handle as well so it doesn't get behind the background, even though it doesn't + // cast shadow. + mHandle.animate() + .setInterpolator(Interpolators.TOUCH_RESPONSE) + .setDuration(TOUCH_ANIMATION_DURATION) + .translationZ(mTouchElevation) + .start(); + mBackgroundLifted = true; + } + + private void releaseBackground() { + if (!mBackgroundLifted) { + return; + } + mBackground.animate() + .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) + .setDuration(TOUCH_RELEASE_ANIMATION_DURATION) + .translationZ(0) + .scaleX(1f) + .scaleY(1f) + .start(); + mHandle.animate() + .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) + .setDuration(TOUCH_RELEASE_ANIMATION_DURATION) + .translationZ(0) + .start(); + mBackgroundLifted = false; + } + + private void initializeSurfaceState() { + int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position; + // Recalculate the split-layout's internal tile bounds + mSplitLayout.resizeSplits(midPos); + Transaction t = mTiles.getTransaction(); + if (mDockedStackMinimized) { + int position = mSplitLayout.getMinimizedSnapAlgorithm(mHomeStackResizable) + .getMiddleTarget().position; + calculateBoundsForPosition(position, mDockSide, mDockedRect); + calculateBoundsForPosition(position, DockedDividerUtils.invertDockSide(mDockSide), + mOtherRect); + mDividerPositionX = mDividerPositionY = position; + resizeSplitSurfaces(t, mDockedRect, mSplitLayout.mPrimary, + mOtherRect, mSplitLayout.mSecondary); + } else { + resizeSplitSurfaces(t, mSplitLayout.mPrimary, null, + mSplitLayout.mSecondary, null); + } + setResizeDimLayer(t, true /* primary */, 0.f /* alpha */); + setResizeDimLayer(t, false /* secondary */, 0.f /* alpha */); + t.apply(); + mTiles.releaseTransaction(t); + + // Get the actually-visible bar dimensions (relative to full window). This is a thin + // bar going through the center. + final Rect dividerBar = isHorizontalDivision() + ? new Rect(0, mDividerInsets, mSplitLayout.mDisplayLayout.width(), + mDividerInsets + mDividerSize) + : new Rect(mDividerInsets, 0, mDividerInsets + mDividerSize, + mSplitLayout.mDisplayLayout.height()); + final Region touchRegion = new Region(dividerBar); + // Add in the "draggable" portion. While not visible, this is an expanded area that the + // user can interact with. + touchRegion.union(new Rect(mHandle.getLeft(), mHandle.getTop(), + mHandle.getRight(), mHandle.getBottom())); + mWindowManager.setTouchRegion(touchRegion); + } + + void setMinimizedDockStack(boolean minimized, boolean isHomeStackResizable, + Transaction t) { + mHomeStackResizable = isHomeStackResizable; + updateDockSide(); + if (!minimized) { + resetBackground(); + } + mMinimizedShadow.setAlpha(minimized ? 1f : 0f); + if (mDockedStackMinimized != minimized) { + mDockedStackMinimized = minimized; + if (mSplitLayout.mDisplayLayout.rotation() != mDefaultDisplay.getRotation()) { + // Splitscreen to minimize is about to starts after rotating landscape to seascape, + // update display info and snap algorithm targets + repositionSnapTargetBeforeMinimized(); + } + if (mIsInMinimizeInteraction != minimized || mCurrentAnimator != null) { + cancelFlingAnimation(); + if (minimized) { + // Relayout to recalculate the divider shadow when minimizing + requestLayout(); + mIsInMinimizeInteraction = true; + resizeStackSurfaces(mSplitLayout.getMinimizedSnapAlgorithm(mHomeStackResizable) + .getMiddleTarget(), t); + } else { + resizeStackSurfaces(mSnapTargetBeforeMinimized, t); + mIsInMinimizeInteraction = false; + } + } + } + } + + void enterSplitMode(boolean isHomeStackResizable) { + post(() -> { + final SurfaceControl sc = getWindowSurfaceControl(); + if (sc == null) { + return; + } + Transaction t = mTiles.getTransaction(); + t.show(sc).apply(); + mTiles.releaseTransaction(t); + }); + + SnapTarget miniMid = + mSplitLayout.getMinimizedSnapAlgorithm(isHomeStackResizable).getMiddleTarget(); + if (mDockedStackMinimized) { + mDividerPositionY = mDividerPositionX = miniMid.position; + } + } + + /** + * Tries to grab a surface control from ViewRootImpl. If this isn't available for some reason + * (ie. the window isn't ready yet), it will get the surfacecontrol that the WindowlessWM has + * assigned to it. + */ + private SurfaceControl getWindowSurfaceControl() { + final ViewRootImpl root = getViewRootImpl(); + if (root == null) { + return null; + } + SurfaceControl out = root.getSurfaceControl(); + if (out != null && out.isValid()) { + return out; + } + return mWindowManager.mSystemWindows.getViewSurface(this); + } + + void exitSplitMode() { + // Reset tile bounds + final SurfaceControl sc = getWindowSurfaceControl(); + if (sc == null) { + return; + } + Transaction t = mTiles.getTransaction(); + t.hide(sc).apply(); + mTiles.releaseTransaction(t); + int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position; + mWindowManagerProxy.applyResizeSplits(midPos, mSplitLayout); + } + + void setMinimizedDockStack(boolean minimized, long animDuration, + boolean isHomeStackResizable) { + if (DEBUG) Slog.d(TAG, "setMinDock: " + mDockedStackMinimized + "->" + minimized); + mHomeStackResizable = isHomeStackResizable; + updateDockSide(); + if (mDockedStackMinimized != minimized) { + mIsInMinimizeInteraction = true; + mDockedStackMinimized = minimized; + stopDragging(minimized + ? mSnapTargetBeforeMinimized.position + : getCurrentPosition(), + minimized + ? mSplitLayout.getMinimizedSnapAlgorithm(mHomeStackResizable) + .getMiddleTarget() + : mSnapTargetBeforeMinimized, + animDuration, Interpolators.FAST_OUT_SLOW_IN, 0); + setAdjustedForIme(false, animDuration); + } + if (!minimized) { + mBackground.animate().withEndAction(mResetBackgroundRunnable); + } + mBackground.animate() + .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) + .setDuration(animDuration) + .start(); + } + + // Needed to end any currently playing animations when they might compete with other anims + // (specifically, IME adjust animation immediately after leaving minimized). Someday maybe + // these can be unified, but not today. + void finishAnimations() { + if (mCurrentAnimator != null) { + mCurrentAnimator.end(); + } + } + + void setAdjustedForIme(boolean adjustedForIme, long animDuration) { + if (mAdjustedForIme == adjustedForIme) { + return; + } + updateDockSide(); + mHandle.animate() + .setInterpolator(IME_ADJUST_INTERPOLATOR) + .setDuration(animDuration) + .alpha(adjustedForIme ? 0f : 1f) + .start(); + if (mDockSide == WindowManager.DOCKED_TOP) { + mBackground.setPivotY(0); + mBackground.animate() + .scaleY(adjustedForIme ? ADJUSTED_FOR_IME_SCALE : 1f); + } + if (!adjustedForIme) { + mBackground.animate().withEndAction(mResetBackgroundRunnable); + } + mBackground.animate() + .setInterpolator(IME_ADJUST_INTERPOLATOR) + .setDuration(animDuration) + .start(); + mAdjustedForIme = adjustedForIme; + } + + private void saveSnapTargetBeforeMinimized(SnapTarget target) { + mSnapTargetBeforeMinimized = target; + mState.mRatioPositionBeforeMinimized = (float) target.position + / (isHorizontalDivision() ? mSplitLayout.mDisplayLayout.height() + : mSplitLayout.mDisplayLayout.width()); + } + + private void resetBackground() { + mBackground.setPivotX(mBackground.getWidth() / 2); + mBackground.setPivotY(mBackground.getHeight() / 2); + mBackground.setScaleX(1f); + mBackground.setScaleY(1f); + mMinimizedShadow.setAlpha(0f); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + } + + private void repositionSnapTargetBeforeMinimized() { + int position = (int) (mState.mRatioPositionBeforeMinimized + * (isHorizontalDivision() ? mSplitLayout.mDisplayLayout.height() + : mSplitLayout.mDisplayLayout.width())); + + // Set the snap target before minimized but do not save until divider is attached and not + // minimized because it does not know its minimized state yet. + mSnapTargetBeforeMinimized = + mSplitLayout.getSnapAlgorithm().calculateNonDismissingSnapTarget(position); + } + + private int calculatePosition(int touchX, int touchY) { + return isHorizontalDivision() ? calculateYPosition(touchY) : calculateXPosition(touchX); + } + + public boolean isHorizontalDivision() { + return getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; + } + + private int calculateXPosition(int touchX) { + return mStartPosition + touchX - mStartX; + } + + private int calculateYPosition(int touchY) { + return mStartPosition + touchY - mStartY; + } + + private void alignTopLeft(Rect containingRect, Rect rect) { + int width = rect.width(); + int height = rect.height(); + rect.set(containingRect.left, containingRect.top, + containingRect.left + width, containingRect.top + height); + } + + private void alignBottomRight(Rect containingRect, Rect rect) { + int width = rect.width(); + int height = rect.height(); + rect.set(containingRect.right - width, containingRect.bottom - height, + containingRect.right, containingRect.bottom); + } + + private void calculateBoundsForPosition(int position, int dockSide, Rect outRect) { + DockedDividerUtils.calculateBoundsForPosition(position, dockSide, outRect, + mSplitLayout.mDisplayLayout.width(), mSplitLayout.mDisplayLayout.height(), + mDividerSize); + } + + private void resizeStackSurfaces(SnapTarget taskSnapTarget, Transaction t) { + resizeStackSurfaces(taskSnapTarget.position, taskSnapTarget.position, taskSnapTarget, t); + } + + void resizeSplitSurfaces(Transaction t, Rect dockedRect, Rect otherRect) { + resizeSplitSurfaces(t, dockedRect, null, otherRect, null); + } + + private void resizeSplitSurfaces(Transaction t, Rect dockedRect, Rect dockedTaskRect, + Rect otherRect, Rect otherTaskRect) { + dockedTaskRect = dockedTaskRect == null ? dockedRect : dockedTaskRect; + otherTaskRect = otherTaskRect == null ? otherRect : otherTaskRect; + + mDividerPositionX = mSplitLayout.getPrimarySplitSide() == DOCKED_RIGHT + ? otherRect.right : dockedRect.right; + mDividerPositionY = dockedRect.bottom; + + if (DEBUG) { + Slog.d(TAG, "Resizing split surfaces: " + dockedRect + " " + dockedTaskRect + + " " + otherRect + " " + otherTaskRect); + } + + t.setPosition(mTiles.mPrimarySurface, dockedTaskRect.left, dockedTaskRect.top); + Rect crop = new Rect(dockedRect); + crop.offsetTo(-Math.min(dockedTaskRect.left - dockedRect.left, 0), + -Math.min(dockedTaskRect.top - dockedRect.top, 0)); + t.setWindowCrop(mTiles.mPrimarySurface, crop); + t.setPosition(mTiles.mSecondarySurface, otherTaskRect.left, otherTaskRect.top); + crop.set(otherRect); + crop.offsetTo(-(otherTaskRect.left - otherRect.left), + -(otherTaskRect.top - otherRect.top)); + t.setWindowCrop(mTiles.mSecondarySurface, crop); + final SurfaceControl dividerCtrl = getWindowSurfaceControl(); + if (dividerCtrl != null) { + if (isHorizontalDivision()) { + t.setPosition(dividerCtrl, 0, mDividerPositionY - mDividerInsets); + } else { + t.setPosition(dividerCtrl, mDividerPositionX - mDividerInsets, 0); + } + } + if (getViewRootImpl() != null) { + mHandler.removeCallbacks(mUpdateEmbeddedMatrix); + mHandler.post(mUpdateEmbeddedMatrix); + } + } + + void setResizeDimLayer(Transaction t, boolean primary, float alpha) { + SurfaceControl dim = primary ? mTiles.mPrimaryDim : mTiles.mSecondaryDim; + if (alpha <= 0.001f) { + t.hide(dim); + } else { + t.setAlpha(dim, alpha); + t.show(dim); + } + } + + void resizeStackSurfaces(int position, int taskPosition, SnapTarget taskSnapTarget, + Transaction transaction) { + if (mRemoved) { + // This divider view has been removed so shouldn't have any additional influence. + return; + } + calculateBoundsForPosition(position, mDockSide, mDockedRect); + calculateBoundsForPosition(position, DockedDividerUtils.invertDockSide(mDockSide), + mOtherRect); + + if (mDockedRect.equals(mLastResizeRect) && !mEntranceAnimationRunning) { + return; + } + + // Make sure shadows are updated + if (mBackground.getZ() > 0f) { + mBackground.invalidate(); + } + + final boolean ownTransaction = transaction == null; + final Transaction t = ownTransaction ? mTiles.getTransaction() : transaction; + mLastResizeRect.set(mDockedRect); + if (mIsInMinimizeInteraction) { + calculateBoundsForPosition(mSnapTargetBeforeMinimized.position, mDockSide, + mDockedTaskRect); + calculateBoundsForPosition(mSnapTargetBeforeMinimized.position, + DockedDividerUtils.invertDockSide(mDockSide), mOtherTaskRect); + + // Move a right-docked-app to line up with the divider while dragging it + if (mDockSide == DOCKED_RIGHT) { + mDockedTaskRect.offset(Math.max(position, -mDividerSize) + - mDockedTaskRect.left + mDividerSize, 0); + } + resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect); + if (ownTransaction) { + t.apply(); + mTiles.releaseTransaction(t); + } + return; + } + + if (mEntranceAnimationRunning && taskPosition != TASK_POSITION_SAME) { + calculateBoundsForPosition(taskPosition, mDockSide, mDockedTaskRect); + + // Move a docked app if from the right in position with the divider up to insets + if (mDockSide == DOCKED_RIGHT) { + mDockedTaskRect.offset(Math.max(position, -mDividerSize) + - mDockedTaskRect.left + mDividerSize, 0); + } + calculateBoundsForPosition(taskPosition, DockedDividerUtils.invertDockSide(mDockSide), + mOtherTaskRect); + resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect); + } else if (mExitAnimationRunning && taskPosition != TASK_POSITION_SAME) { + calculateBoundsForPosition(taskPosition, mDockSide, mDockedTaskRect); + mDockedInsetRect.set(mDockedTaskRect); + calculateBoundsForPosition(mExitStartPosition, + DockedDividerUtils.invertDockSide(mDockSide), mOtherTaskRect); + mOtherInsetRect.set(mOtherTaskRect); + applyExitAnimationParallax(mOtherTaskRect, position); + + // Move a right-docked-app to line up with the divider while dragging it + if (mDockSide == DOCKED_RIGHT) { + mDockedTaskRect.offset(position + mDividerSize, 0); + } + resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect); + } else if (taskPosition != TASK_POSITION_SAME) { + calculateBoundsForPosition(position, DockedDividerUtils.invertDockSide(mDockSide), + mOtherRect); + int dockSideInverted = DockedDividerUtils.invertDockSide(mDockSide); + int taskPositionDocked = + restrictDismissingTaskPosition(taskPosition, mDockSide, taskSnapTarget); + int taskPositionOther = + restrictDismissingTaskPosition(taskPosition, dockSideInverted, taskSnapTarget); + calculateBoundsForPosition(taskPositionDocked, mDockSide, mDockedTaskRect); + calculateBoundsForPosition(taskPositionOther, dockSideInverted, mOtherTaskRect); + mTmpRect.set(0, 0, mSplitLayout.mDisplayLayout.width(), + mSplitLayout.mDisplayLayout.height()); + alignTopLeft(mDockedRect, mDockedTaskRect); + alignTopLeft(mOtherRect, mOtherTaskRect); + mDockedInsetRect.set(mDockedTaskRect); + mOtherInsetRect.set(mOtherTaskRect); + if (dockSideTopLeft(mDockSide)) { + alignTopLeft(mTmpRect, mDockedInsetRect); + alignBottomRight(mTmpRect, mOtherInsetRect); + } else { + alignBottomRight(mTmpRect, mDockedInsetRect); + alignTopLeft(mTmpRect, mOtherInsetRect); + } + applyDismissingParallax(mDockedTaskRect, mDockSide, taskSnapTarget, position, + taskPositionDocked); + applyDismissingParallax(mOtherTaskRect, dockSideInverted, taskSnapTarget, position, + taskPositionOther); + resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect); + } else { + resizeSplitSurfaces(t, mDockedRect, null, mOtherRect, null); + } + SnapTarget closestDismissTarget = getSnapAlgorithm().getClosestDismissTarget(position); + float dimFraction = getDimFraction(position, closestDismissTarget); + setResizeDimLayer(t, isDismissTargetPrimary(closestDismissTarget), dimFraction); + if (ownTransaction) { + t.apply(); + mTiles.releaseTransaction(t); + } + } + + private void applyExitAnimationParallax(Rect taskRect, int position) { + if (mDockSide == WindowManager.DOCKED_TOP) { + taskRect.offset(0, (int) ((position - mExitStartPosition) * 0.25f)); + } else if (mDockSide == WindowManager.DOCKED_LEFT) { + taskRect.offset((int) ((position - mExitStartPosition) * 0.25f), 0); + } else if (mDockSide == WindowManager.DOCKED_RIGHT) { + taskRect.offset((int) ((mExitStartPosition - position) * 0.25f), 0); + } + } + + private float getDimFraction(int position, SnapTarget dismissTarget) { + if (mEntranceAnimationRunning) { + return 0f; + } + float fraction = getSnapAlgorithm().calculateDismissingFraction(position); + fraction = Math.max(0, Math.min(fraction, 1f)); + fraction = DIM_INTERPOLATOR.getInterpolation(fraction); + return fraction; + } + + /** + * When the snap target is dismissing one side, make sure that the dismissing side doesn't get + * 0 size. + */ + private int restrictDismissingTaskPosition(int taskPosition, int dockSide, + SnapTarget snapTarget) { + if (snapTarget.flag == SnapTarget.FLAG_DISMISS_START && dockSideTopLeft(dockSide)) { + return Math.max(mSplitLayout.getSnapAlgorithm().getFirstSplitTarget().position, + mStartPosition); + } else if (snapTarget.flag == SnapTarget.FLAG_DISMISS_END + && dockSideBottomRight(dockSide)) { + return Math.min(mSplitLayout.getSnapAlgorithm().getLastSplitTarget().position, + mStartPosition); + } else { + return taskPosition; + } + } + + /** + * Applies a parallax to the task when dismissing. + */ + private void applyDismissingParallax(Rect taskRect, int dockSide, SnapTarget snapTarget, + int position, int taskPosition) { + float fraction = Math.min(1, Math.max(0, + mSplitLayout.getSnapAlgorithm().calculateDismissingFraction(position))); + SnapTarget dismissTarget = null; + SnapTarget splitTarget = null; + int start = 0; + if (position <= mSplitLayout.getSnapAlgorithm().getLastSplitTarget().position + && dockSideTopLeft(dockSide)) { + dismissTarget = mSplitLayout.getSnapAlgorithm().getDismissStartTarget(); + splitTarget = mSplitLayout.getSnapAlgorithm().getFirstSplitTarget(); + start = taskPosition; + } else if (position >= mSplitLayout.getSnapAlgorithm().getLastSplitTarget().position + && dockSideBottomRight(dockSide)) { + dismissTarget = mSplitLayout.getSnapAlgorithm().getDismissEndTarget(); + splitTarget = mSplitLayout.getSnapAlgorithm().getLastSplitTarget(); + start = splitTarget.position; + } + if (dismissTarget != null && fraction > 0f + && isDismissing(splitTarget, position, dockSide)) { + fraction = calculateParallaxDismissingFraction(fraction, dockSide); + int offsetPosition = (int) (start + fraction + * (dismissTarget.position - splitTarget.position)); + int width = taskRect.width(); + int height = taskRect.height(); + switch (dockSide) { + case WindowManager.DOCKED_LEFT: + taskRect.left = offsetPosition - width; + taskRect.right = offsetPosition; + break; + case WindowManager.DOCKED_RIGHT: + taskRect.left = offsetPosition + mDividerSize; + taskRect.right = offsetPosition + width + mDividerSize; + break; + case WindowManager.DOCKED_TOP: + taskRect.top = offsetPosition - height; + taskRect.bottom = offsetPosition; + break; + case WindowManager.DOCKED_BOTTOM: + taskRect.top = offsetPosition + mDividerSize; + taskRect.bottom = offsetPosition + height + mDividerSize; + break; + } + } + } + + /** + * @return for a specified {@code fraction}, this returns an adjusted value that simulates a + * slowing down parallax effect + */ + private static float calculateParallaxDismissingFraction(float fraction, int dockSide) { + float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f; + + // Less parallax at the top, just because. + if (dockSide == WindowManager.DOCKED_TOP) { + result /= 2f; + } + return result; + } + + private static boolean isDismissing(SnapTarget snapTarget, int position, int dockSide) { + if (dockSide == WindowManager.DOCKED_TOP || dockSide == WindowManager.DOCKED_LEFT) { + return position < snapTarget.position; + } else { + return position > snapTarget.position; + } + } + + private boolean isDismissTargetPrimary(SnapTarget dismissTarget) { + return (dismissTarget.flag == SnapTarget.FLAG_DISMISS_START && dockSideTopLeft(mDockSide)) + || (dismissTarget.flag == SnapTarget.FLAG_DISMISS_END + && dockSideBottomRight(mDockSide)); + } + + /** + * @return true if and only if {@code dockSide} is top or left + */ + private static boolean dockSideTopLeft(int dockSide) { + return dockSide == WindowManager.DOCKED_TOP || dockSide == WindowManager.DOCKED_LEFT; + } + + /** + * @return true if and only if {@code dockSide} is bottom or right + */ + private static boolean dockSideBottomRight(int dockSide) { + return dockSide == WindowManager.DOCKED_BOTTOM || dockSide == WindowManager.DOCKED_RIGHT; + } + + @Override + public void onComputeInternalInsets(InternalInsetsInfo inoutInfo) { + inoutInfo.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + inoutInfo.touchableRegion.set(mHandle.getLeft(), mHandle.getTop(), mHandle.getRight(), + mHandle.getBottom()); + inoutInfo.touchableRegion.op(mBackground.getLeft(), mBackground.getTop(), + mBackground.getRight(), mBackground.getBottom(), Op.UNION); + } + + void onDockedFirstAnimationFrame() { + saveSnapTargetBeforeMinimized(mSplitLayout.getSnapAlgorithm().getMiddleTarget()); + } + + void onDockedTopTask() { + mState.animateAfterRecentsDrawn = true; + startDragging(false /* animate */, false /* touching */); + updateDockSide(); + mEntranceAnimationRunning = true; + + resizeStackSurfaces(calculatePositionForInsetBounds(), + mSplitLayout.getSnapAlgorithm().getMiddleTarget().position, + mSplitLayout.getSnapAlgorithm().getMiddleTarget(), + null /* transaction */); + } + + void onRecentsDrawn() { + updateDockSide(); + final int position = calculatePositionForInsetBounds(); + if (mState.animateAfterRecentsDrawn) { + mState.animateAfterRecentsDrawn = false; + + mHandler.post(() -> { + // Delay switching resizing mode because this might cause jank in recents animation + // that's longer than this animation. + stopDragging(position, getSnapAlgorithm().getMiddleTarget(), + mLongPressEntraceAnimDuration, Interpolators.FAST_OUT_SLOW_IN, + 200 /* endDelay */); + }); + } + } + + void onUndockingTask() { + int dockSide = mSplitLayout.getPrimarySplitSide(); + if (inSplitMode()) { + startDragging(false /* animate */, false /* touching */); + SnapTarget target = dockSideTopLeft(dockSide) + ? mSplitLayout.getSnapAlgorithm().getDismissEndTarget() + : mSplitLayout.getSnapAlgorithm().getDismissStartTarget(); + + // Don't start immediately - give a little bit time to settle the drag resize change. + mExitAnimationRunning = true; + mExitStartPosition = getCurrentPosition(); + stopDragging(mExitStartPosition, target, 336 /* duration */, 100 /* startDelay */, + 0 /* endDelay */, Interpolators.FAST_OUT_SLOW_IN); + } + } + + private int calculatePositionForInsetBounds() { + mSplitLayout.mDisplayLayout.getStableBounds(mTmpRect); + return DockedDividerUtils.calculatePositionForBounds(mTmpRect, mDockSide, mDividerSize); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerWindowManager.java new file mode 100644 index 000000000000..0b4e17c27398 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerWindowManager.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.splitscreen; + +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; +import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; +import static android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; +import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; +import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; +import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; + +import android.graphics.PixelFormat; +import android.graphics.Region; +import android.os.Binder; +import android.view.View; +import android.view.WindowManager; + +import com.android.wm.shell.common.SystemWindows; + +/** + * Manages the window parameters of the docked stack divider. + */ +final class DividerWindowManager { + + private static final String WINDOW_TITLE = "DockedStackDivider"; + + final SystemWindows mSystemWindows; + private WindowManager.LayoutParams mLp; + private View mView; + + DividerWindowManager(SystemWindows systemWindows) { + mSystemWindows = systemWindows; + } + + /** Add a divider view */ + void add(View view, int width, int height, int displayId) { + mLp = new WindowManager.LayoutParams( + width, height, TYPE_DOCK_DIVIDER, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL + | FLAG_WATCH_OUTSIDE_TOUCH | FLAG_SPLIT_TOUCH | FLAG_SLIPPERY, + PixelFormat.TRANSLUCENT); + mLp.token = new Binder(); + mLp.setTitle(WINDOW_TITLE); + mLp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION; + mLp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + mSystemWindows.addView(view, mLp, displayId, TYPE_DOCK_DIVIDER); + mView = view; + } + + void remove() { + if (mView != null) { + mSystemWindows.removeView(mView); + } + mView = null; + } + + void setSlippery(boolean slippery) { + boolean changed = false; + if (slippery && (mLp.flags & FLAG_SLIPPERY) == 0) { + mLp.flags |= FLAG_SLIPPERY; + changed = true; + } else if (!slippery && (mLp.flags & FLAG_SLIPPERY) != 0) { + mLp.flags &= ~FLAG_SLIPPERY; + changed = true; + } + if (changed) { + mSystemWindows.updateViewLayout(mView, mLp); + } + } + + void setTouchable(boolean touchable) { + if (mView == null) { + return; + } + boolean changed = false; + if (!touchable && (mLp.flags & FLAG_NOT_TOUCHABLE) == 0) { + mLp.flags |= FLAG_NOT_TOUCHABLE; + changed = true; + } else if (touchable && (mLp.flags & FLAG_NOT_TOUCHABLE) != 0) { + mLp.flags &= ~FLAG_NOT_TOUCHABLE; + changed = true; + } + if (changed) { + mSystemWindows.updateViewLayout(mView, mLp); + } + } + + /** Sets the touch region to `touchRegion`. Use null to unset.*/ + void setTouchRegion(Region touchRegion) { + if (mView == null) { + return; + } + mSystemWindows.setTouchableRegion(mView, touchRegion); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ForcedResizableInfoActivity.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ForcedResizableInfoActivity.java new file mode 100644 index 000000000000..7a1633530148 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ForcedResizableInfoActivity.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.splitscreen; + +import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SECONDARY_DISPLAY; +import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN; + +import android.annotation.Nullable; +import android.app.Activity; +import android.app.ActivityManager; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; +import android.widget.TextView; + +import com.android.wm.shell.R; + +/** + * Translucent activity that gets started on top of a task in multi-window to inform the user that + * we forced the activity below to be resizable. + */ +public class ForcedResizableInfoActivity extends Activity implements OnTouchListener { + + public static final String EXTRA_FORCED_RESIZEABLE_REASON = "extra_forced_resizeable_reason"; + + private static final long DISMISS_DELAY = 2500; + + private final Runnable mFinishRunnable = new Runnable() { + @Override + public void run() { + finish(); + } + }; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.forced_resizable_activity); + TextView tv = findViewById(com.android.internal.R.id.message); + int reason = getIntent().getIntExtra(EXTRA_FORCED_RESIZEABLE_REASON, -1); + String text; + switch (reason) { + case FORCED_RESIZEABLE_REASON_SPLIT_SCREEN: + text = getString(R.string.dock_forced_resizable); + break; + case FORCED_RESIZEABLE_REASON_SECONDARY_DISPLAY: + text = getString(R.string.forced_resizable_secondary_display); + break; + default: + throw new IllegalArgumentException("Unexpected forced resizeable reason: " + + reason); + } + tv.setText(text); + getWindow().setTitle(text); + getWindow().getDecorView().setOnTouchListener(this); + } + + @Override + protected void onStart() { + super.onStart(); + getWindow().getDecorView().postDelayed(mFinishRunnable, DISMISS_DELAY); + } + + @Override + protected void onStop() { + super.onStop(); + finish(); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + finish(); + return true; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + finish(); + return true; + } + + @Override + public void finish() { + super.finish(); + overridePendingTransition(0, R.anim.forced_resizable_exit); + } + + @Override + public void setTaskDescription(ActivityManager.TaskDescription taskDescription) { + // Do nothing + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ForcedResizableInfoActivityController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ForcedResizableInfoActivityController.java new file mode 100644 index 000000000000..1ef142dacb9e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ForcedResizableInfoActivityController.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.splitscreen; + + +import static com.android.wm.shell.splitscreen.ForcedResizableInfoActivity.EXTRA_FORCED_RESIZEABLE_REASON; + +import android.app.ActivityOptions; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.UserHandle; +import android.util.ArraySet; +import android.widget.Toast; + +import com.android.wm.shell.R; + +import java.util.function.Consumer; + +/** + * Controller that decides when to show the {@link ForcedResizableInfoActivity}. + */ +final class ForcedResizableInfoActivityController implements DividerView.DividerCallbacks { + + private static final String SELF_PACKAGE_NAME = "com.android.systemui"; + + private static final int TIMEOUT = 1000; + private final Context mContext; + private final Handler mHandler = new Handler(); + private final ArraySet<PendingTaskRecord> mPendingTasks = new ArraySet<>(); + private final ArraySet<String> mPackagesShownInSession = new ArraySet<>(); + private boolean mDividerDragging; + + private final Runnable mTimeoutRunnable = this::showPending; + + private final Consumer<Boolean> mDockedStackExistsListener = exists -> { + if (!exists) { + mPackagesShownInSession.clear(); + } + }; + + /** Record of force resized task that's pending to be handled. */ + private class PendingTaskRecord { + int mTaskId; + /** + * {@link android.app.ITaskStackListener#FORCED_RESIZEABLE_REASON_SPLIT_SCREEN} or + * {@link android.app.ITaskStackListener#FORCED_RESIZEABLE_REASON_SECONDARY_DISPLAY} + */ + int mReason; + + PendingTaskRecord(int taskId, int reason) { + this.mTaskId = taskId; + this.mReason = reason; + } + } + + ForcedResizableInfoActivityController(Context context, + SplitScreenController splitScreenController) { + mContext = context; + splitScreenController.registerInSplitScreenListener(mDockedStackExistsListener); + } + + @Override + public void onDraggingStart() { + mDividerDragging = true; + mHandler.removeCallbacks(mTimeoutRunnable); + } + + @Override + public void onDraggingEnd() { + mDividerDragging = false; + showPending(); + } + + void onAppTransitionFinished() { + if (!mDividerDragging) { + showPending(); + } + } + + void activityForcedResizable(String packageName, int taskId, int reason) { + if (debounce(packageName)) { + return; + } + mPendingTasks.add(new PendingTaskRecord(taskId, reason)); + postTimeout(); + } + + void activityDismissingSplitScreen() { + Toast.makeText(mContext, R.string.dock_non_resizeble_failed_to_dock_text, + Toast.LENGTH_SHORT).show(); + } + + void activityLaunchOnSecondaryDisplayFailed() { + Toast.makeText(mContext, R.string.activity_launch_on_secondary_display_failed_text, + Toast.LENGTH_SHORT).show(); + } + + private void showPending() { + mHandler.removeCallbacks(mTimeoutRunnable); + for (int i = mPendingTasks.size() - 1; i >= 0; i--) { + PendingTaskRecord pendingRecord = mPendingTasks.valueAt(i); + Intent intent = new Intent(mContext, ForcedResizableInfoActivity.class); + ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchTaskId(pendingRecord.mTaskId); + // Set as task overlay and allow to resume, so that when an app enters split-screen and + // becomes paused, the overlay will still be shown. + options.setTaskOverlay(true, true /* canResume */); + intent.putExtra(EXTRA_FORCED_RESIZEABLE_REASON, pendingRecord.mReason); + mContext.startActivityAsUser(intent, options.toBundle(), UserHandle.CURRENT); + } + mPendingTasks.clear(); + } + + private void postTimeout() { + mHandler.removeCallbacks(mTimeoutRunnable); + mHandler.postDelayed(mTimeoutRunnable, TIMEOUT); + } + + private boolean debounce(String packageName) { + if (packageName == null) { + return false; + } + + // We launch ForcedResizableInfoActivity into a task that was forced resizable, so that + // triggers another notification. So ignore our own activity. + if (SELF_PACKAGE_NAME.equals(packageName)) { + return true; + } + boolean debounce = mPackagesShownInSession.contains(packageName); + mPackagesShownInSession.add(packageName); + return debounce; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MinimizedDockShadow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MinimizedDockShadow.java new file mode 100644 index 000000000000..06f4ef109193 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MinimizedDockShadow.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.splitscreen; + +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Shader; +import android.util.AttributeSet; +import android.view.View; +import android.view.WindowManager; + +import com.android.wm.shell.R; + +/** + * Shadow for the minimized dock state on homescreen. + */ +public class MinimizedDockShadow extends View { + + private final Paint mShadowPaint = new Paint(); + + private int mDockSide = WindowManager.DOCKED_INVALID; + + public MinimizedDockShadow(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + void setDockSide(int dockSide) { + if (dockSide != mDockSide) { + mDockSide = dockSide; + updatePaint(getLeft(), getTop(), getRight(), getBottom()); + invalidate(); + } + } + + private void updatePaint(int left, int top, int right, int bottom) { + int startColor = mContext.getResources().getColor( + R.color.minimize_dock_shadow_start, null); + int endColor = mContext.getResources().getColor( + R.color.minimize_dock_shadow_end, null); + final int middleColor = Color.argb( + (Color.alpha(startColor) + Color.alpha(endColor)) / 2, 0, 0, 0); + final int quarter = Color.argb( + (int) (Color.alpha(startColor) * 0.25f + Color.alpha(endColor) * 0.75f), + 0, 0, 0); + if (mDockSide == WindowManager.DOCKED_TOP) { + mShadowPaint.setShader(new LinearGradient( + 0, 0, 0, bottom - top, + new int[] { startColor, middleColor, quarter, endColor }, + new float[] { 0f, 0.35f, 0.6f, 1f }, Shader.TileMode.CLAMP)); + } else if (mDockSide == WindowManager.DOCKED_LEFT) { + mShadowPaint.setShader(new LinearGradient( + 0, 0, right - left, 0, + new int[] { startColor, middleColor, quarter, endColor }, + new float[] { 0f, 0.35f, 0.6f, 1f }, Shader.TileMode.CLAMP)); + } else if (mDockSide == WindowManager.DOCKED_RIGHT) { + mShadowPaint.setShader(new LinearGradient( + right - left, 0, 0, 0, + new int[] { startColor, middleColor, quarter, endColor }, + new float[] { 0f, 0.35f, 0.6f, 1f }, Shader.TileMode.CLAMP)); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (changed) { + updatePaint(left, top, right, bottom); + invalidate(); + } + } + + @Override + protected void onDraw(Canvas canvas) { + canvas.drawRect(0, 0, getWidth(), getHeight(), mShadowPaint); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitDisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitDisplayLayout.java new file mode 100644 index 000000000000..3c0f93906795 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitDisplayLayout.java @@ -0,0 +1,313 @@ +/* + * 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.wm.shell.splitscreen; + +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.view.WindowManager.DOCKED_BOTTOM; +import static android.view.WindowManager.DOCKED_INVALID; +import static android.view.WindowManager.DOCKED_LEFT; +import static android.view.WindowManager.DOCKED_RIGHT; +import static android.view.WindowManager.DOCKED_TOP; + +import android.annotation.NonNull; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Rect; +import android.util.TypedValue; +import android.window.WindowContainerTransaction; + +import com.android.internal.policy.DividerSnapAlgorithm; +import com.android.internal.policy.DockedDividerUtils; +import com.android.wm.shell.common.DisplayLayout; + +/** + * Handles split-screen related internal display layout. In general, this represents the + * WM-facing understanding of the splits. + */ +public class SplitDisplayLayout { + /** Minimum size of an adjusted stack bounds relative to original stack bounds. Used to + * restrict IME adjustment so that a min portion of top stack remains visible.*/ + private static final float ADJUSTED_STACK_FRACTION_MIN = 0.3f; + + private static final int DIVIDER_WIDTH_INACTIVE_DP = 4; + + SplitScreenTaskOrganizer mTiles; + DisplayLayout mDisplayLayout; + Context mContext; + + // Lazy stuff + boolean mResourcesValid = false; + int mDividerSize; + int mDividerSizeInactive; + private DividerSnapAlgorithm mSnapAlgorithm = null; + private DividerSnapAlgorithm mMinimizedSnapAlgorithm = null; + Rect mPrimary = null; + Rect mSecondary = null; + Rect mAdjustedPrimary = null; + Rect mAdjustedSecondary = null; + + public SplitDisplayLayout(Context ctx, DisplayLayout dl, SplitScreenTaskOrganizer taskTiles) { + mTiles = taskTiles; + mDisplayLayout = dl; + mContext = ctx; + } + + void rotateTo(int newRotation) { + mDisplayLayout.rotateTo(mContext.getResources(), newRotation); + final Configuration config = new Configuration(); + config.unset(); + config.orientation = mDisplayLayout.getOrientation(); + Rect tmpRect = new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); + tmpRect.inset(mDisplayLayout.nonDecorInsets()); + config.windowConfiguration.setAppBounds(tmpRect); + tmpRect.set(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); + tmpRect.inset(mDisplayLayout.stableInsets()); + config.screenWidthDp = (int) (tmpRect.width() / mDisplayLayout.density()); + config.screenHeightDp = (int) (tmpRect.height() / mDisplayLayout.density()); + mContext = mContext.createConfigurationContext(config); + mSnapAlgorithm = null; + mMinimizedSnapAlgorithm = null; + mResourcesValid = false; + } + + private void updateResources() { + if (mResourcesValid) { + return; + } + mResourcesValid = true; + Resources res = mContext.getResources(); + mDividerSize = DockedDividerUtils.getDividerSize(res, + DockedDividerUtils.getDividerInsets(res)); + mDividerSizeInactive = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, DIVIDER_WIDTH_INACTIVE_DP, res.getDisplayMetrics()); + } + + int getPrimarySplitSide() { + switch (mDisplayLayout.getNavigationBarPosition(mContext.getResources())) { + case DisplayLayout.NAV_BAR_BOTTOM: + return mDisplayLayout.isLandscape() ? DOCKED_LEFT : DOCKED_TOP; + case DisplayLayout.NAV_BAR_LEFT: + return DOCKED_RIGHT; + case DisplayLayout.NAV_BAR_RIGHT: + return DOCKED_LEFT; + default: + return DOCKED_INVALID; + } + } + + DividerSnapAlgorithm getSnapAlgorithm() { + if (mSnapAlgorithm == null) { + updateResources(); + boolean isHorizontalDivision = !mDisplayLayout.isLandscape(); + mSnapAlgorithm = new DividerSnapAlgorithm(mContext.getResources(), + mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize, + isHorizontalDivision, mDisplayLayout.stableInsets(), getPrimarySplitSide()); + } + return mSnapAlgorithm; + } + + DividerSnapAlgorithm getMinimizedSnapAlgorithm(boolean homeStackResizable) { + if (mMinimizedSnapAlgorithm == null) { + updateResources(); + boolean isHorizontalDivision = !mDisplayLayout.isLandscape(); + mMinimizedSnapAlgorithm = new DividerSnapAlgorithm(mContext.getResources(), + mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize, + isHorizontalDivision, mDisplayLayout.stableInsets(), getPrimarySplitSide(), + true /* isMinimized */, homeStackResizable); + } + return mMinimizedSnapAlgorithm; + } + + void resizeSplits(int position) { + mPrimary = mPrimary == null ? new Rect() : mPrimary; + mSecondary = mSecondary == null ? new Rect() : mSecondary; + calcSplitBounds(position, mPrimary, mSecondary); + } + + void resizeSplits(int position, WindowContainerTransaction t) { + resizeSplits(position); + t.setBounds(mTiles.mPrimary.token, mPrimary); + t.setBounds(mTiles.mSecondary.token, mSecondary); + + t.setSmallestScreenWidthDp(mTiles.mPrimary.token, + getSmallestWidthDpForBounds(mContext, mDisplayLayout, mPrimary)); + t.setSmallestScreenWidthDp(mTiles.mSecondary.token, + getSmallestWidthDpForBounds(mContext, mDisplayLayout, mSecondary)); + } + + void calcSplitBounds(int position, @NonNull Rect outPrimary, @NonNull Rect outSecondary) { + int dockSide = getPrimarySplitSide(); + DockedDividerUtils.calculateBoundsForPosition(position, dockSide, outPrimary, + mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize); + + DockedDividerUtils.calculateBoundsForPosition(position, + DockedDividerUtils.invertDockSide(dockSide), outSecondary, mDisplayLayout.width(), + mDisplayLayout.height(), mDividerSize); + } + + Rect calcResizableMinimizedHomeStackBounds() { + DividerSnapAlgorithm.SnapTarget miniMid = + getMinimizedSnapAlgorithm(true /* resizable */).getMiddleTarget(); + Rect homeBounds = new Rect(); + DockedDividerUtils.calculateBoundsForPosition(miniMid.position, + DockedDividerUtils.invertDockSide(getPrimarySplitSide()), homeBounds, + mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize); + return homeBounds; + } + + /** + * Updates the adjustment depending on it's current state. + */ + void updateAdjustedBounds(int currImeTop, int hiddenTop, int shownTop) { + adjustForIME(mDisplayLayout, currImeTop, hiddenTop, shownTop, mDividerSize, + mDividerSizeInactive, mPrimary, mSecondary); + } + + /** Assumes top/bottom split. Splits are not adjusted for left/right splits. */ + private void adjustForIME(DisplayLayout dl, int currImeTop, int hiddenTop, int shownTop, + int dividerWidth, int dividerWidthInactive, Rect primaryBounds, Rect secondaryBounds) { + if (mAdjustedPrimary == null) { + mAdjustedPrimary = new Rect(); + mAdjustedSecondary = new Rect(); + } + + final Rect displayStableRect = new Rect(); + dl.getStableBounds(displayStableRect); + + final float shownFraction = ((float) (currImeTop - hiddenTop)) / (shownTop - hiddenTop); + final int currDividerWidth = + (int) (dividerWidthInactive * shownFraction + dividerWidth * (1.f - shownFraction)); + + // Calculate the highest we can move the bottom of the top stack to keep 30% visible. + final int minTopStackBottom = displayStableRect.top + + (int) ((mPrimary.bottom - displayStableRect.top) * ADJUSTED_STACK_FRACTION_MIN); + // Based on that, calculate the maximum amount we'll allow the ime to shift things. + final int maxOffset = mPrimary.bottom - minTopStackBottom; + // Calculate how much we would shift things without limits (basically the height of ime). + final int desiredOffset = hiddenTop - shownTop; + // Calculate an "adjustedTop" which is the currImeTop but restricted by our constraints. + // We want an effect where the adjustment only occurs during the "highest" portion of the + // ime animation. This is done by shifting the adjustment values by the difference in + // offsets (effectively playing the whole adjustment animation some fixed amount of pixels + // below the ime top). + final int topCorrection = Math.max(0, desiredOffset - maxOffset); + final int adjustedTop = currImeTop + topCorrection; + // The actual yOffset is the distance between adjustedTop and the bottom of the display. + // Since our adjustedTop values are playing "below" the ime, we clamp at 0 so we only + // see adjustment upward. + final int yOffset = Math.max(0, dl.height() - adjustedTop); + + // TOP + // Reduce the offset by an additional small amount to squish the divider bar. + mAdjustedPrimary.set(primaryBounds); + mAdjustedPrimary.offset(0, -yOffset + (dividerWidth - currDividerWidth)); + + // BOTTOM + mAdjustedSecondary.set(secondaryBounds); + mAdjustedSecondary.offset(0, -yOffset); + } + + static int getSmallestWidthDpForBounds(@NonNull Context context, DisplayLayout dl, + Rect bounds) { + int dividerSize = DockedDividerUtils.getDividerSize(context.getResources(), + DockedDividerUtils.getDividerInsets(context.getResources())); + + int minWidth = Integer.MAX_VALUE; + + // Go through all screen orientations and find the orientation in which the task has the + // smallest width. + Rect tmpRect = new Rect(); + Rect rotatedDisplayRect = new Rect(); + Rect displayRect = new Rect(0, 0, dl.width(), dl.height()); + + DisplayLayout tmpDL = new DisplayLayout(); + for (int rotation = 0; rotation < 4; rotation++) { + tmpDL.set(dl); + tmpDL.rotateTo(context.getResources(), rotation); + DividerSnapAlgorithm snap = initSnapAlgorithmForRotation(context, tmpDL, dividerSize); + + tmpRect.set(bounds); + DisplayLayout.rotateBounds(tmpRect, displayRect, rotation - dl.rotation()); + rotatedDisplayRect.set(0, 0, tmpDL.width(), tmpDL.height()); + final int dockSide = getPrimarySplitSide(tmpRect, rotatedDisplayRect, + tmpDL.getOrientation()); + final int position = DockedDividerUtils.calculatePositionForBounds(tmpRect, dockSide, + dividerSize); + + final int snappedPosition = + snap.calculateNonDismissingSnapTarget(position).position; + DockedDividerUtils.calculateBoundsForPosition(snappedPosition, dockSide, tmpRect, + tmpDL.width(), tmpDL.height(), dividerSize); + Rect insettedDisplay = new Rect(rotatedDisplayRect); + insettedDisplay.inset(tmpDL.stableInsets()); + tmpRect.intersect(insettedDisplay); + minWidth = Math.min(tmpRect.width(), minWidth); + } + return (int) (minWidth / dl.density()); + } + + static DividerSnapAlgorithm initSnapAlgorithmForRotation(Context context, DisplayLayout dl, + int dividerSize) { + final Configuration config = new Configuration(); + config.unset(); + config.orientation = dl.getOrientation(); + Rect tmpRect = new Rect(0, 0, dl.width(), dl.height()); + tmpRect.inset(dl.nonDecorInsets()); + config.windowConfiguration.setAppBounds(tmpRect); + tmpRect.set(0, 0, dl.width(), dl.height()); + tmpRect.inset(dl.stableInsets()); + config.screenWidthDp = (int) (tmpRect.width() / dl.density()); + config.screenHeightDp = (int) (tmpRect.height() / dl.density()); + final Context rotationContext = context.createConfigurationContext(config); + return new DividerSnapAlgorithm( + rotationContext.getResources(), dl.width(), dl.height(), dividerSize, + config.orientation == ORIENTATION_PORTRAIT, dl.stableInsets()); + } + + /** + * Get the current primary-split side. Determined by its location of {@param bounds} within + * {@param displayRect} but if both are the same, it will try to dock to each side and determine + * if allowed in its respected {@param orientation}. + * + * @param bounds bounds of the primary split task to get which side is docked + * @param displayRect bounds of the display that contains the primary split task + * @param orientation the origination of device + * @return current primary-split side + */ + static int getPrimarySplitSide(Rect bounds, Rect displayRect, int orientation) { + if (orientation == ORIENTATION_PORTRAIT) { + // Portrait mode, docked either at the top or the bottom. + final int diff = (displayRect.bottom - bounds.bottom) - (bounds.top - displayRect.top); + if (diff < 0) { + return DOCKED_BOTTOM; + } else { + // Top is default + return DOCKED_TOP; + } + } else if (orientation == ORIENTATION_LANDSCAPE) { + // Landscape mode, docked either on the left or on the right. + final int diff = (displayRect.right - bounds.right) - (bounds.left - displayRect.left); + if (diff < 0) { + return DOCKED_RIGHT; + } + return DOCKED_LEFT; + } + return DOCKED_INVALID; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java new file mode 100644 index 000000000000..184342f14d4f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java @@ -0,0 +1,91 @@ +/* + * 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.wm.shell.splitscreen; + +import android.graphics.Rect; +import android.window.WindowContainerToken; + +import java.io.PrintWriter; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Interface to engage split screen feature. + */ +public interface SplitScreen { + /** Returns {@code true} if split screen is supported on the device. */ + boolean isSplitScreenSupported(); + + /** Called when keyguard showing state changed. */ + void onKeyguardVisibilityChanged(boolean isShowing); + + /** Returns {@link DividerView}. */ + DividerView getDividerView(); + + /** Returns {@code true} if one of the split screen is in minimized mode. */ + boolean isMinimized(); + + /** Returns {@code true} if the home stack is resizable. */ + boolean isHomeStackResizable(); + + /** Returns {@code true} if the divider is visible. */ + boolean isDividerVisible(); + + /** Switch to minimized state if appropriate. */ + void setMinimized(boolean minimized); + + /** + * Workaround for b/62528361, at the time recents has drawn, it may happen before a + * configuration change to the Divider, and internally, the event will be posted to the + * subscriber, or DividerView, which has been removed and prevented from resizing. Instead, + * register the event handler here and proxy the event to the current DividerView. + */ + void onRecentsDrawn(); + + /** Called when there's an activity forced resizable. */ + void onActivityForcedResizable(String packageName, int taskId, int reason); + + /** Called when there's an activity dismissing split screen. */ + void onActivityDismissingSplitScreen(); + + /** Called when there's an activity launch on secondary display failed. */ + void onActivityLaunchOnSecondaryDisplayFailed(); + + /** Called when there's a task undocking. */ + void onUndockingTask(); + + /** Called when the first docked animation frame rendered. */ + void onDockedFirstAnimationFrame(); + + /** Called when top task docked. */ + void onDockedTopTask(); + + /** Called when app transition finished. */ + void onAppTransitionFinished(); + + /** Dumps current status of Split Screen. */ + void dump(PrintWriter pw); + + /** Registers listener that gets called whenever the existence of the divider changes. */ + void registerInSplitScreenListener(Consumer<Boolean> listener); + + /** Registers listener that gets called whenever the split screen bounds changes. */ + void registerBoundsChangeListener(BiConsumer<Rect, Rect> listener); + + /** @return the container token for the secondary split root task. */ + WindowContainerToken getSecondaryRoot(); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java new file mode 100644 index 000000000000..eed5092ea96b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -0,0 +1,546 @@ +/* + * 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.wm.shell.splitscreen; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.view.Display.DEFAULT_DISPLAY; + +import android.app.ActivityTaskManager; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.Handler; +import android.provider.Settings; +import android.util.Slog; +import android.view.LayoutInflater; +import android.view.View; +import android.window.TaskOrganizer; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.internal.policy.DividerSnapAlgorithm; +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayChangeController; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.common.TransactionPool; + +import java.io.PrintWriter; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Controls split screen feature. + */ +public class SplitScreenController implements SplitScreen, + DisplayController.OnDisplaysChangedListener { + static final boolean DEBUG = false; + + private static final String TAG = "Divider"; + private static final int DEFAULT_APP_TRANSITION_DURATION = 336; + + private final Context mContext; + private final DisplayChangeController.OnDisplayChangingListener mRotationController; + private final DisplayController mDisplayController; + private final DisplayImeController mImeController; + private final DividerImeController mImePositionProcessor; + private final DividerState mDividerState = new DividerState(); + private final ForcedResizableInfoActivityController mForcedResizableController; + private final Handler mHandler; + private final SplitScreenTaskOrganizer mSplits; + private final SystemWindows mSystemWindows; + final TransactionPool mTransactionPool; + private final WindowManagerProxy mWindowManagerProxy; + private final TaskOrganizer mTaskOrganizer; + + private final ArrayList<WeakReference<Consumer<Boolean>>> mDockedStackExistsListeners = + new ArrayList<>(); + private final ArrayList<WeakReference<BiConsumer<Rect, Rect>>> mBoundsChangedListeners = + new ArrayList<>(); + + + private DividerWindowManager mWindowManager; + private DividerView mView; + + // Keeps track of real-time split geometry including snap positions and ime adjustments + private SplitDisplayLayout mSplitLayout; + + // Transient: this contains the layout calculated for a new rotation requested by WM. This is + // kept around so that we can wait for a matching configuration change and then use the exact + // layout that we sent back to WM. + private SplitDisplayLayout mRotateSplitLayout; + + private boolean mIsKeyguardShowing; + private boolean mVisible = false; + private boolean mMinimized = false; + private boolean mAdjustedForIme = false; + private boolean mHomeStackResizable = false; + + public SplitScreenController(Context context, + DisplayController displayController, SystemWindows systemWindows, + DisplayImeController imeController, Handler handler, TransactionPool transactionPool, + ShellTaskOrganizer shellTaskOrganizer) { + mContext = context; + mDisplayController = displayController; + mSystemWindows = systemWindows; + mImeController = imeController; + mHandler = handler; + mForcedResizableController = new ForcedResizableInfoActivityController(context, this); + mTransactionPool = transactionPool; + mWindowManagerProxy = new WindowManagerProxy(mTransactionPool, mHandler, + shellTaskOrganizer); + mTaskOrganizer = shellTaskOrganizer; + mSplits = new SplitScreenTaskOrganizer(this, shellTaskOrganizer); + mImePositionProcessor = new DividerImeController(mSplits, mTransactionPool, mHandler, + shellTaskOrganizer); + mRotationController = + (display, fromRotation, toRotation, wct) -> { + if (!mSplits.isSplitScreenSupported() || mWindowManagerProxy == null) { + return; + } + WindowContainerTransaction t = new WindowContainerTransaction(); + DisplayLayout displayLayout = + new DisplayLayout(mDisplayController.getDisplayLayout(display)); + SplitDisplayLayout sdl = + new SplitDisplayLayout(mContext, displayLayout, mSplits); + sdl.rotateTo(toRotation); + mRotateSplitLayout = sdl; + final int position = isDividerVisible() + ? (mMinimized ? mView.mSnapTargetBeforeMinimized.position + : mView.getCurrentPosition()) + // snap resets to middle target when not in split-mode + : sdl.getSnapAlgorithm().getMiddleTarget().position; + DividerSnapAlgorithm snap = sdl.getSnapAlgorithm(); + final DividerSnapAlgorithm.SnapTarget target = + snap.calculateNonDismissingSnapTarget(position); + sdl.resizeSplits(target.position, t); + + if (isSplitActive() && mHomeStackResizable) { + mWindowManagerProxy + .applyHomeTasksMinimized(sdl, mSplits.mSecondary.token, t); + } + if (mWindowManagerProxy.queueSyncTransactionIfWaiting(t)) { + // Because sync transactions are serialized, its possible for an "older" + // bounds-change to get applied after a screen rotation. In that case, we + // want to actually defer on that rather than apply immediately. Of course, + // this means that the bounds may not change until after the rotation so + // the user might see some artifacts. This should be rare. + Slog.w(TAG, "Screen rotated while other operations were pending, this may" + + " result in some graphical artifacts."); + } else { + wct.merge(t, true /* transfer */); + } + }; + + mWindowManager = new DividerWindowManager(mSystemWindows); + mDisplayController.addDisplayWindowListener(this); + // Don't initialize the divider or anything until we get the default display. + } + + /** Returns {@code true} if split screen is supported on the device. */ + public boolean isSplitScreenSupported() { + return mSplits.isSplitScreenSupported(); + } + + /** Called when keyguard showing state changed. */ + public void onKeyguardVisibilityChanged(boolean showing) { + if (!isSplitActive() || mView == null) { + return; + } + mView.setHidden(showing); + if (!showing) { + mImePositionProcessor.updateAdjustForIme(); + } + mIsKeyguardShowing = showing; + } + + @Override + public void onDisplayAdded(int displayId) { + if (displayId != DEFAULT_DISPLAY) { + return; + } + mSplitLayout = new SplitDisplayLayout(mDisplayController.getDisplayContext(displayId), + mDisplayController.getDisplayLayout(displayId), mSplits); + mImeController.addPositionProcessor(mImePositionProcessor); + mDisplayController.addDisplayChangingController(mRotationController); + if (!ActivityTaskManager.supportsSplitScreenMultiWindow(mContext)) { + removeDivider(); + return; + } + try { + mSplits.init(); + // Set starting tile bounds based on middle target + final WindowContainerTransaction tct = new WindowContainerTransaction(); + int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position; + mSplitLayout.resizeSplits(midPos, tct); + mTaskOrganizer.applyTransaction(tct); + } catch (Exception e) { + Slog.e(TAG, "Failed to register docked stack listener", e); + removeDivider(); + return; + } + } + + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + if (displayId != DEFAULT_DISPLAY || !mSplits.isSplitScreenSupported()) { + return; + } + mSplitLayout = new SplitDisplayLayout(mDisplayController.getDisplayContext(displayId), + mDisplayController.getDisplayLayout(displayId), mSplits); + if (mRotateSplitLayout == null) { + int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position; + final WindowContainerTransaction tct = new WindowContainerTransaction(); + mSplitLayout.resizeSplits(midPos, tct); + mTaskOrganizer.applyTransaction(tct); + } else if (mSplitLayout.mDisplayLayout.rotation() + == mRotateSplitLayout.mDisplayLayout.rotation()) { + mSplitLayout.mPrimary = new Rect(mRotateSplitLayout.mPrimary); + mSplitLayout.mSecondary = new Rect(mRotateSplitLayout.mSecondary); + mRotateSplitLayout = null; + } + if (isSplitActive()) { + update(newConfig); + } + } + + /** Posts task to handler dealing with divider. */ + void post(Runnable task) { + mHandler.post(task); + } + + /** Returns {@link DividerView}. */ + public DividerView getDividerView() { + return mView; + } + + /** Returns {@code true} if one of the split screen is in minimized mode. */ + public boolean isMinimized() { + return mMinimized; + } + + public boolean isHomeStackResizable() { + return mHomeStackResizable; + } + + /** Returns {@code true} if the divider is visible. */ + public boolean isDividerVisible() { + return mView != null && mView.getVisibility() == View.VISIBLE; + } + + /** + * This indicates that at-least one of the splits has content. This differs from + * isDividerVisible because the divider is only visible once *everything* is in split mode + * while this only cares if some things are (eg. while entering/exiting as well). + */ + private boolean isSplitActive() { + return mSplits.mPrimary != null && mSplits.mSecondary != null + && (mSplits.mPrimary.topActivityType != ACTIVITY_TYPE_UNDEFINED + || mSplits.mSecondary.topActivityType != ACTIVITY_TYPE_UNDEFINED); + } + + private void addDivider(Configuration configuration) { + Context dctx = mDisplayController.getDisplayContext(mContext.getDisplayId()); + mView = (DividerView) + LayoutInflater.from(dctx).inflate(R.layout.docked_stack_divider, null); + DisplayLayout displayLayout = mDisplayController.getDisplayLayout(mContext.getDisplayId()); + mView.injectDependencies(this, mWindowManager, mDividerState, mForcedResizableController, + mSplits, mSplitLayout, mImePositionProcessor, mWindowManagerProxy); + mView.setVisibility(mVisible ? View.VISIBLE : View.INVISIBLE); + mView.setMinimizedDockStack(mMinimized, mHomeStackResizable, null /* transaction */); + final int size = dctx.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.docked_stack_divider_thickness); + final boolean landscape = configuration.orientation == ORIENTATION_LANDSCAPE; + final int width = landscape ? size : displayLayout.width(); + final int height = landscape ? displayLayout.height() : size; + mWindowManager.add(mView, width, height, mContext.getDisplayId()); + } + + private void removeDivider() { + if (mView != null) { + mView.onDividerRemoved(); + } + mWindowManager.remove(); + } + + private void update(Configuration configuration) { + final boolean isDividerHidden = mView != null && mIsKeyguardShowing; + + removeDivider(); + addDivider(configuration); + + if (mMinimized) { + mView.setMinimizedDockStack(true, mHomeStackResizable, null /* transaction */); + updateTouchable(); + } + mView.setHidden(isDividerHidden); + } + + void onTaskVanished() { + mHandler.post(this::removeDivider); + } + + private void updateVisibility(final boolean visible) { + if (DEBUG) Slog.d(TAG, "Updating visibility " + mVisible + "->" + visible); + if (mVisible != visible) { + mVisible = visible; + mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); + + if (visible) { + mView.enterSplitMode(mHomeStackResizable); + // Update state because animations won't finish. + mWindowManagerProxy.runInSync( + t -> mView.setMinimizedDockStack(mMinimized, mHomeStackResizable, t)); + + } else { + mView.exitSplitMode(); + mWindowManagerProxy.runInSync( + t -> mView.setMinimizedDockStack(false, mHomeStackResizable, t)); + } + // Notify existence listeners + synchronized (mDockedStackExistsListeners) { + mDockedStackExistsListeners.removeIf(wf -> { + Consumer<Boolean> l = wf.get(); + if (l != null) l.accept(visible); + return l == null; + }); + } + } + } + + /** Switch to minimized state if appropriate. */ + public void setMinimized(final boolean minimized) { + if (DEBUG) Slog.d(TAG, "posting ext setMinimized " + minimized + " vis:" + mVisible); + mHandler.post(() -> { + if (DEBUG) Slog.d(TAG, "run posted ext setMinimized " + minimized + " vis:" + mVisible); + if (!mVisible) { + return; + } + setHomeMinimized(minimized); + }); + } + + private void setHomeMinimized(final boolean minimized) { + if (DEBUG) { + Slog.d(TAG, "setHomeMinimized min:" + mMinimized + "->" + minimized + " hrsz:" + + mHomeStackResizable + " split:" + isDividerVisible()); + } + WindowContainerTransaction wct = new WindowContainerTransaction(); + final boolean minimizedChanged = mMinimized != minimized; + // Update minimized state + if (minimizedChanged) { + mMinimized = minimized; + } + // Always set this because we could be entering split when mMinimized is already true + wct.setFocusable(mSplits.mPrimary.token, !mMinimized); + + // Sync state to DividerView if it exists. + if (mView != null) { + final int displayId = mView.getDisplay() != null + ? mView.getDisplay().getDisplayId() : DEFAULT_DISPLAY; + // pause ime here (before updateMinimizedDockedStack) + if (mMinimized) { + mImePositionProcessor.pause(displayId); + } + if (minimizedChanged) { + // This conflicts with IME adjustment, so only call it when things change. + mView.setMinimizedDockStack(minimized, getAnimDuration(), mHomeStackResizable); + } + if (!mMinimized) { + // afterwards so it can end any animations started in view + mImePositionProcessor.resume(displayId); + } + } + updateTouchable(); + + // If we are only setting focusability, a sync transaction isn't necessary (in fact it + // can interrupt other animations), so see if it can be submitted on pending instead. + if (!mWindowManagerProxy.queueSyncTransactionIfWaiting(wct)) { + mTaskOrganizer.applyTransaction(wct); + } + } + + void setAdjustedForIme(boolean adjustedForIme) { + if (mAdjustedForIme == adjustedForIme) { + return; + } + mAdjustedForIme = adjustedForIme; + updateTouchable(); + } + + private void updateTouchable() { + mWindowManager.setTouchable(!mAdjustedForIme); + } + + /** + * Workaround for b/62528361, at the time recents has drawn, it may happen before a + * configuration change to the Divider, and internally, the event will be posted to the + * subscriber, or DividerView, which has been removed and prevented from resizing. Instead, + * register the event handler here and proxy the event to the current DividerView. + */ + public void onRecentsDrawn() { + if (mView != null) { + mView.onRecentsDrawn(); + } + } + + /** Called when there's an activity forced resizable. */ + public void onActivityForcedResizable(String packageName, int taskId, int reason) { + mForcedResizableController.activityForcedResizable(packageName, taskId, reason); + } + + /** Called when there's an activity dismissing split screen. */ + public void onActivityDismissingSplitScreen() { + mForcedResizableController.activityDismissingSplitScreen(); + } + + /** Called when there's an activity launch on secondary display failed. */ + public void onActivityLaunchOnSecondaryDisplayFailed() { + mForcedResizableController.activityLaunchOnSecondaryDisplayFailed(); + } + + /** Called when there's a task undocking. */ + public void onUndockingTask() { + if (mView != null) { + mView.onUndockingTask(); + } + } + + /** Called when the first docked animation frame rendered. */ + public void onDockedFirstAnimationFrame() { + if (mView != null) { + mView.onDockedFirstAnimationFrame(); + } + } + + /** Called when top task docked. */ + public void onDockedTopTask() { + if (mView != null) { + mView.onDockedTopTask(); + } + } + + /** Called when app transition finished. */ + public void onAppTransitionFinished() { + if (mView == null) { + return; + } + mForcedResizableController.onAppTransitionFinished(); + } + + /** Dumps current status of Split Screen. */ + public void dump(PrintWriter pw) { + pw.print(" mVisible="); pw.println(mVisible); + pw.print(" mMinimized="); pw.println(mMinimized); + pw.print(" mAdjustedForIme="); pw.println(mAdjustedForIme); + } + + long getAnimDuration() { + float transitionScale = Settings.Global.getFloat(mContext.getContentResolver(), + Settings.Global.TRANSITION_ANIMATION_SCALE, + mContext.getResources().getFloat( + com.android.internal.R.dimen + .config_appTransitionAnimationDurationScaleDefault)); + final long transitionDuration = DEFAULT_APP_TRANSITION_DURATION; + return (long) (transitionDuration * transitionScale); + } + + /** Registers listener that gets called whenever the existence of the divider changes. */ + public void registerInSplitScreenListener(Consumer<Boolean> listener) { + listener.accept(isDividerVisible()); + synchronized (mDockedStackExistsListeners) { + mDockedStackExistsListeners.add(new WeakReference<>(listener)); + } + } + + @Override + public void registerBoundsChangeListener(BiConsumer<Rect, Rect> listener) { + synchronized (mBoundsChangedListeners) { + mBoundsChangedListeners.add(new WeakReference<>(listener)); + } + } + + /** Notifies the bounds of split screen changed. */ + void notifyBoundsChanged(Rect secondaryWindowBounds, Rect secondaryWindowInsets) { + synchronized (mBoundsChangedListeners) { + mBoundsChangedListeners.removeIf(wf -> { + BiConsumer<Rect, Rect> l = wf.get(); + if (l != null) l.accept(secondaryWindowBounds, secondaryWindowInsets); + return l == null; + }); + } + } + + void startEnterSplit() { + update(mDisplayController.getDisplayContext( + mContext.getDisplayId()).getResources().getConfiguration()); + // Set resizable directly here because applyEnterSplit already resizes home stack. + mHomeStackResizable = mWindowManagerProxy.applyEnterSplit(mSplits, mSplitLayout); + } + + void startDismissSplit() { + mWindowManagerProxy.applyDismissSplit(mSplits, mSplitLayout, true /* dismissOrMaximize */); + updateVisibility(false /* visible */); + mMinimized = false; + removeDivider(); + mImePositionProcessor.reset(); + } + + void ensureMinimizedSplit() { + setHomeMinimized(true /* minimized */); + if (mView != null && !isDividerVisible()) { + // Wasn't in split-mode yet, so enter now. + if (DEBUG) { + Slog.d(TAG, " entering split mode with minimized=true"); + } + updateVisibility(true /* visible */); + } + } + + void ensureNormalSplit() { + setHomeMinimized(false /* minimized */); + if (mView != null && !isDividerVisible()) { + // Wasn't in split-mode, so enter now. + if (DEBUG) { + Slog.d(TAG, " enter split mode unminimized "); + } + updateVisibility(true /* visible */); + } + } + + SplitDisplayLayout getSplitLayout() { + return mSplitLayout; + } + + WindowManagerProxy getWmProxy() { + return mWindowManagerProxy; + } + + /** @return the container token for the secondary split root task. */ + public WindowContainerToken getSecondaryRoot() { + if (mSplits == null || mSplits.mSecondary == null) { + return null; + } + return mSplits.mSecondary.token; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTaskOrganizer.java new file mode 100644 index 000000000000..30bc43b0292f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTaskOrganizer.java @@ -0,0 +1,232 @@ +/* + * 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.wm.shell.splitscreen; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; +import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; +import static android.view.Display.DEFAULT_DISPLAY; + +import android.app.ActivityManager.RunningTaskInfo; +import android.app.WindowConfiguration; +import android.graphics.Rect; +import android.os.RemoteException; +import android.util.Log; +import android.view.Display; +import android.view.SurfaceControl; +import android.view.SurfaceSession; + +import com.android.wm.shell.ShellTaskOrganizer; + +class SplitScreenTaskOrganizer implements ShellTaskOrganizer.TaskListener { + private static final String TAG = "SplitScreenTaskOrg"; + private static final boolean DEBUG = SplitScreenController.DEBUG; + + private final ShellTaskOrganizer mTaskOrganizer; + + RunningTaskInfo mPrimary; + RunningTaskInfo mSecondary; + SurfaceControl mPrimarySurface; + SurfaceControl mSecondarySurface; + SurfaceControl mPrimaryDim; + SurfaceControl mSecondaryDim; + Rect mHomeBounds = new Rect(); + final SplitScreenController mSplitScreenController; + private boolean mSplitScreenSupported = false; + + final SurfaceSession mSurfaceSession = new SurfaceSession(); + + SplitScreenTaskOrganizer(SplitScreenController splitScreenController, + ShellTaskOrganizer shellTaskOrganizer) { + mSplitScreenController = splitScreenController; + mTaskOrganizer = shellTaskOrganizer; + mTaskOrganizer.addListener(this, WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, + WINDOWING_MODE_SPLIT_SCREEN_SECONDARY); + } + + void init() throws RemoteException { + synchronized (this) { + try { + mPrimary = mTaskOrganizer.createRootTask(Display.DEFAULT_DISPLAY, + WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY); + mSecondary = mTaskOrganizer.createRootTask(Display.DEFAULT_DISPLAY, + WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY); + } catch (Exception e) { + // teardown to prevent callbacks + mTaskOrganizer.removeListener(this); + throw e; + } + } + } + + boolean isSplitScreenSupported() { + return mSplitScreenSupported; + } + + SurfaceControl.Transaction getTransaction() { + return mSplitScreenController.mTransactionPool.acquire(); + } + + void releaseTransaction(SurfaceControl.Transaction t) { + mSplitScreenController.mTransactionPool.release(t); + } + + @Override + public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { + synchronized (this) { + if (mPrimary == null || mSecondary == null) { + Log.w(TAG, "Received onTaskAppeared before creating root tasks " + taskInfo); + return; + } + + if (taskInfo.token.equals(mPrimary.token)) { + mPrimarySurface = leash; + } else if (taskInfo.token.equals(mSecondary.token)) { + mSecondarySurface = leash; + } + + if (!mSplitScreenSupported && mPrimarySurface != null && mSecondarySurface != null) { + mSplitScreenSupported = true; + + // Initialize dim surfaces: + mPrimaryDim = new SurfaceControl.Builder(mSurfaceSession) + .setParent(mPrimarySurface).setColorLayer() + .setName("Primary Divider Dim") + .setCallsite("SplitScreenTaskOrganizer.onTaskAppeared") + .build(); + mSecondaryDim = new SurfaceControl.Builder(mSurfaceSession) + .setParent(mSecondarySurface).setColorLayer() + .setName("Secondary Divider Dim") + .setCallsite("SplitScreenTaskOrganizer.onTaskAppeared") + .build(); + SurfaceControl.Transaction t = getTransaction(); + t.setLayer(mPrimaryDim, Integer.MAX_VALUE); + t.setColor(mPrimaryDim, new float[]{0f, 0f, 0f}); + t.setLayer(mSecondaryDim, Integer.MAX_VALUE); + t.setColor(mSecondaryDim, new float[]{0f, 0f, 0f}); + t.apply(); + releaseTransaction(t); + } + } + } + + @Override + public void onTaskVanished(RunningTaskInfo taskInfo) { + synchronized (this) { + final boolean isPrimaryTask = mPrimary != null + && taskInfo.token.equals(mPrimary.token); + final boolean isSecondaryTask = mSecondary != null + && taskInfo.token.equals(mSecondary.token); + + if (mSplitScreenSupported && (isPrimaryTask || isSecondaryTask)) { + mSplitScreenSupported = false; + + SurfaceControl.Transaction t = getTransaction(); + t.remove(mPrimaryDim); + t.remove(mSecondaryDim); + t.remove(mPrimarySurface); + t.remove(mSecondarySurface); + t.apply(); + releaseTransaction(t); + + mSplitScreenController.onTaskVanished(); + } + } + } + + @Override + public void onTaskInfoChanged(RunningTaskInfo taskInfo) { + if (taskInfo.displayId != DEFAULT_DISPLAY) { + return; + } + mSplitScreenController.post(() -> handleTaskInfoChanged(taskInfo)); + } + + /** + * This is effectively a finite state machine which moves between the various split-screen + * presentations based on the contents of the split regions. + */ + private void handleTaskInfoChanged(RunningTaskInfo info) { + if (!mSplitScreenSupported) { + // This shouldn't happen; but apparently there is a chance that SysUI crashes without + // system server receiving binder-death (or maybe it receives binder-death too late?). + // In this situation, when sys-ui restarts, the split root-tasks will still exist so + // there is a small window of time during init() where WM might send messages here + // before init() fails. So, avoid a cycle of crashes by returning early. + Log.e(TAG, "Got handleTaskInfoChanged when not initialized: " + info); + return; + } + final boolean secondaryImpliedMinimize = mSecondary.topActivityType == ACTIVITY_TYPE_HOME + || (mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS + && mSplitScreenController.isHomeStackResizable()); + final boolean primaryWasEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED; + final boolean secondaryWasEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED; + if (info.token.asBinder() == mPrimary.token.asBinder()) { + mPrimary = info; + } else if (info.token.asBinder() == mSecondary.token.asBinder()) { + mSecondary = info; + } + final boolean primaryIsEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED; + final boolean secondaryIsEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED; + final boolean secondaryImpliesMinimize = mSecondary.topActivityType == ACTIVITY_TYPE_HOME + || (mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS + && mSplitScreenController.isHomeStackResizable()); + if (DEBUG) { + Log.d(TAG, "onTaskInfoChanged " + mPrimary + " " + mSecondary); + } + if (primaryIsEmpty == primaryWasEmpty && secondaryWasEmpty == secondaryIsEmpty + && secondaryImpliedMinimize == secondaryImpliesMinimize) { + // No relevant changes + return; + } + if (primaryIsEmpty || secondaryIsEmpty) { + // At-least one of the splits is empty which means we are currently transitioning + // into or out-of split-screen mode. + if (DEBUG) { + Log.d(TAG, " at-least one split empty " + mPrimary.topActivityType + + " " + mSecondary.topActivityType); + } + if (mSplitScreenController.isDividerVisible()) { + // Was in split-mode, which means we are leaving split, so continue that. + // This happens when the stack in the primary-split is dismissed. + if (DEBUG) { + Log.d(TAG, " was in split, so this means leave it " + + mPrimary.topActivityType + " " + mSecondary.topActivityType); + } + mSplitScreenController.startDismissSplit(); + } else if (!primaryIsEmpty && primaryWasEmpty && secondaryWasEmpty) { + // Wasn't in split-mode (both were empty), but now that the primary split is + // populated, we should fully enter split by moving everything else into secondary. + // This just tells window-manager to reparent things, the UI will respond + // when it gets new task info for the secondary split. + if (DEBUG) { + Log.d(TAG, " was not in split, but primary is populated, so enter it"); + } + mSplitScreenController.startEnterSplit(); + } + } else if (secondaryImpliesMinimize) { + // Both splits are populated but the secondary split has a home/recents stack on top, + // so enter minimized mode. + mSplitScreenController.ensureMinimizedSplit(); + } else { + // Both splits are populated by normal activities, so make sure we aren't minimized. + mSplitScreenController.ensureNormalSplit(); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/WindowManagerProxy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/WindowManagerProxy.java new file mode 100644 index 000000000000..015707ecc6c8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/WindowManagerProxy.java @@ -0,0 +1,369 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.splitscreen; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_UNDEFINED; +import static android.view.Display.DEFAULT_DISPLAY; + +import android.annotation.NonNull; +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.graphics.Rect; +import android.os.Handler; +import android.os.RemoteException; +import android.util.Log; +import android.view.Display; +import android.view.SurfaceControl; +import android.view.WindowManagerGlobal; +import android.window.TaskOrganizer; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; +import android.window.WindowOrganizer; + +import com.android.internal.annotations.GuardedBy; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.TransactionPool; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Proxy to simplify calls into window manager/activity manager + */ +class WindowManagerProxy { + + private static final String TAG = "WindowManagerProxy"; + private static final int[] HOME_AND_RECENTS = {ACTIVITY_TYPE_HOME, ACTIVITY_TYPE_RECENTS}; + + @GuardedBy("mDockedRect") + private final Rect mDockedRect = new Rect(); + + private final Rect mTmpRect1 = new Rect(); + + @GuardedBy("mDockedRect") + private final Rect mTouchableRegion = new Rect(); + + private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); + + private final SyncTransactionQueue mSyncTransactionQueue; + + private final Runnable mSetTouchableRegionRunnable = new Runnable() { + @Override + public void run() { + try { + synchronized (mDockedRect) { + mTmpRect1.set(mTouchableRegion); + } + WindowManagerGlobal.getWindowManagerService().setDockedStackDividerTouchRegion( + mTmpRect1); + } catch (RemoteException e) { + Log.w(TAG, "Failed to set touchable region: " + e); + } + } + }; + + private final TaskOrganizer mTaskOrganizer; + + WindowManagerProxy(TransactionPool transactionPool, Handler handler, + TaskOrganizer taskOrganizer) { + mSyncTransactionQueue = new SyncTransactionQueue(transactionPool, handler); + mTaskOrganizer = taskOrganizer; + } + + void dismissOrMaximizeDocked(final SplitScreenTaskOrganizer tiles, SplitDisplayLayout layout, + final boolean dismissOrMaximize) { + mExecutor.execute(() -> applyDismissSplit(tiles, layout, dismissOrMaximize)); + } + + public void setResizing(final boolean resizing) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + try { + ActivityTaskManager.getService().setSplitScreenResizing(resizing); + } catch (RemoteException e) { + Log.w(TAG, "Error calling setDockedStackResizing: " + e); + } + } + }); + } + + /** Sets a touch region */ + public void setTouchRegion(Rect region) { + synchronized (mDockedRect) { + mTouchableRegion.set(region); + } + mExecutor.execute(mSetTouchableRegionRunnable); + } + + void applyResizeSplits(int position, SplitDisplayLayout splitLayout) { + WindowContainerTransaction t = new WindowContainerTransaction(); + splitLayout.resizeSplits(position, t); + new WindowOrganizer().applyTransaction(t); + } + + private boolean getHomeAndRecentsTasks(List<ActivityManager.RunningTaskInfo> out, + WindowContainerToken parent) { + boolean resizable = false; + List<ActivityManager.RunningTaskInfo> rootTasks = parent == null + ? mTaskOrganizer.getRootTasks(Display.DEFAULT_DISPLAY, HOME_AND_RECENTS) + : mTaskOrganizer.getChildTasks(parent, HOME_AND_RECENTS); + for (int i = 0, n = rootTasks.size(); i < n; ++i) { + final ActivityManager.RunningTaskInfo ti = rootTasks.get(i); + out.add(ti); + if (ti.topActivityType == ACTIVITY_TYPE_HOME) { + resizable = ti.isResizeable; + } + } + return resizable; + } + + /** + * Assign a fixed override-bounds to home tasks that reflect their geometry while the primary + * split is minimized. This actually "sticks out" of the secondary split area, but when in + * minimized mode, the secondary split gets a 'negative' crop to expose it. + */ + boolean applyHomeTasksMinimized(SplitDisplayLayout layout, WindowContainerToken parent, + @NonNull WindowContainerTransaction wct) { + // Resize the home/recents stacks to the larger minimized-state size + final Rect homeBounds; + final ArrayList<ActivityManager.RunningTaskInfo> homeStacks = new ArrayList<>(); + boolean isHomeResizable = getHomeAndRecentsTasks(homeStacks, parent); + if (isHomeResizable) { + homeBounds = layout.calcResizableMinimizedHomeStackBounds(); + } else { + // home is not resizable, so lock it to its inherent orientation size. + homeBounds = new Rect(0, 0, 0, 0); + for (int i = homeStacks.size() - 1; i >= 0; --i) { + if (homeStacks.get(i).topActivityType == ACTIVITY_TYPE_HOME) { + final int orient = homeStacks.get(i).configuration.orientation; + final boolean displayLandscape = layout.mDisplayLayout.isLandscape(); + final boolean isLandscape = orient == ORIENTATION_LANDSCAPE + || (orient == ORIENTATION_UNDEFINED && displayLandscape); + homeBounds.right = isLandscape == displayLandscape + ? layout.mDisplayLayout.width() : layout.mDisplayLayout.height(); + homeBounds.bottom = isLandscape == displayLandscape + ? layout.mDisplayLayout.height() : layout.mDisplayLayout.width(); + break; + } + } + } + for (int i = homeStacks.size() - 1; i >= 0; --i) { + // For non-resizable homes, the minimized size is actually the fullscreen-size. As a + // result, we don't minimize for recents since it only shows half-size screenshots. + if (!isHomeResizable) { + if (homeStacks.get(i).topActivityType == ACTIVITY_TYPE_RECENTS) { + continue; + } + wct.setWindowingMode(homeStacks.get(i).token, WINDOWING_MODE_FULLSCREEN); + } + wct.setBounds(homeStacks.get(i).token, homeBounds); + } + layout.mTiles.mHomeBounds.set(homeBounds); + return isHomeResizable; + } + + /** + * Finishes entering split-screen by reparenting all FULLSCREEN tasks into the secondary split. + * This assumes there is already something in the primary split since that is usually what + * triggers a call to this. In the same transaction, this overrides the home task bounds via + * {@link #applyHomeTasksMinimized}. + * + * @return whether the home stack is resizable + */ + boolean applyEnterSplit(SplitScreenTaskOrganizer tiles, SplitDisplayLayout layout) { + // Set launchtile first so that any stack created after + // getAllStackInfos and before reparent (even if unlikely) are placed + // correctly. + mTaskOrganizer.setLaunchRoot(DEFAULT_DISPLAY, tiles.mSecondary.token); + List<ActivityManager.RunningTaskInfo> rootTasks = + mTaskOrganizer.getRootTasks(DEFAULT_DISPLAY, null /* activityTypes */); + WindowContainerTransaction wct = new WindowContainerTransaction(); + if (rootTasks.isEmpty()) { + return false; + } + ActivityManager.RunningTaskInfo topHomeTask = null; + for (int i = rootTasks.size() - 1; i >= 0; --i) { + final ActivityManager.RunningTaskInfo rootTask = rootTasks.get(i); + // Only move resizeable task to split secondary. However, we have an exception + // for non-resizable home because we will minimize to show it. + if (!rootTask.isResizeable && rootTask.topActivityType != ACTIVITY_TYPE_HOME) { + continue; + } + // Only move fullscreen tasks to split secondary. + if (rootTask.configuration.windowConfiguration.getWindowingMode() + != WINDOWING_MODE_FULLSCREEN) { + continue; + } + // Since this iterates from bottom to top, update topHomeTask for every fullscreen task + // so it will be left with the status of the top one. + topHomeTask = isHomeOrRecentTask(rootTask) ? rootTask : null; + wct.reparent(rootTask.token, tiles.mSecondary.token, true /* onTop */); + } + // Move the secondary split-forward. + wct.reorder(tiles.mSecondary.token, true /* onTop */); + boolean isHomeResizable = applyHomeTasksMinimized(layout, null /* parent */, wct); + if (topHomeTask != null) { + // Translate/update-crop of secondary out-of-band with sync transaction -- Until BALST + // is enabled, this temporarily syncs the home surface position with offset until + // sync transaction finishes. + wct.setBoundsChangeTransaction(topHomeTask.token, tiles.mHomeBounds); + } + applySyncTransaction(wct); + return isHomeResizable; + } + + boolean isHomeOrRecentTask(ActivityManager.RunningTaskInfo ti) { + final int atype = ti.configuration.windowConfiguration.getActivityType(); + return atype == ACTIVITY_TYPE_HOME || atype == ACTIVITY_TYPE_RECENTS; + } + + /** + * Reparents all tile members back to their display and resets home task override bounds. + * @param dismissOrMaximize When {@code true} this resolves the split by closing the primary + * split (thus resulting in the top of the secondary split becoming + * fullscreen. {@code false} resolves the other way. + */ + void applyDismissSplit(SplitScreenTaskOrganizer tiles, SplitDisplayLayout layout, + boolean dismissOrMaximize) { + // Set launch root first so that any task created after getChildContainers and + // before reparent (pretty unlikely) are put into fullscreen. + mTaskOrganizer.setLaunchRoot(Display.DEFAULT_DISPLAY, null); + // TODO(task-org): Once task-org is more complete, consider using Appeared/Vanished + // plus specific APIs to clean this up. + List<ActivityManager.RunningTaskInfo> primaryChildren = + mTaskOrganizer.getChildTasks(tiles.mPrimary.token, null /* activityTypes */); + List<ActivityManager.RunningTaskInfo> secondaryChildren = + mTaskOrganizer.getChildTasks(tiles.mSecondary.token, null /* activityTypes */); + // In some cases (eg. non-resizable is launched), system-server will leave split-screen. + // as a result, the above will not capture any tasks; yet, we need to clean-up the + // home task bounds. + List<ActivityManager.RunningTaskInfo> freeHomeAndRecents = + mTaskOrganizer.getRootTasks(DEFAULT_DISPLAY, HOME_AND_RECENTS); + // Filter out the root split tasks + freeHomeAndRecents.removeIf(p -> p.token.equals(tiles.mSecondary.token) + || p.token.equals(tiles.mPrimary.token)); + + if (primaryChildren.isEmpty() && secondaryChildren.isEmpty() + && freeHomeAndRecents.isEmpty()) { + return; + } + WindowContainerTransaction wct = new WindowContainerTransaction(); + if (dismissOrMaximize) { + // Dismissing, so move all primary split tasks first + for (int i = primaryChildren.size() - 1; i >= 0; --i) { + wct.reparent(primaryChildren.get(i).token, null /* parent */, + true /* onTop */); + } + boolean homeOnTop = false; + // Don't need to worry about home tasks because they are already in the "proper" + // order within the secondary split. + for (int i = secondaryChildren.size() - 1; i >= 0; --i) { + final ActivityManager.RunningTaskInfo ti = secondaryChildren.get(i); + wct.reparent(ti.token, null /* parent */, true /* onTop */); + if (isHomeOrRecentTask(ti)) { + wct.setBounds(ti.token, null); + wct.setWindowingMode(ti.token, WINDOWING_MODE_UNDEFINED); + if (i == 0) { + homeOnTop = true; + } + } + } + if (homeOnTop) { + // Translate/update-crop of secondary out-of-band with sync transaction -- instead + // play this in sync with new home-app frame because until BALST is enabled this + // shows up on screen before the syncTransaction returns. + // We only have access to the secondary root surface, though, so in order to + // position things properly, we have to take into account the existing negative + // offset/crop of the minimized-home task. + final boolean landscape = layout.mDisplayLayout.isLandscape(); + final int posX = landscape ? layout.mSecondary.left - tiles.mHomeBounds.left + : layout.mSecondary.left; + final int posY = landscape ? layout.mSecondary.top + : layout.mSecondary.top - tiles.mHomeBounds.top; + final SurfaceControl.Transaction sft = new SurfaceControl.Transaction(); + sft.setPosition(tiles.mSecondarySurface, posX, posY); + final Rect crop = new Rect(0, 0, layout.mDisplayLayout.width(), + layout.mDisplayLayout.height()); + crop.offset(-posX, -posY); + sft.setWindowCrop(tiles.mSecondarySurface, crop); + wct.setBoundsChangeTransaction(tiles.mSecondary.token, sft); + } + } else { + // Maximize, so move non-home secondary split first + for (int i = secondaryChildren.size() - 1; i >= 0; --i) { + if (isHomeOrRecentTask(secondaryChildren.get(i))) { + continue; + } + wct.reparent(secondaryChildren.get(i).token, null /* parent */, + true /* onTop */); + } + // Find and place home tasks in-between. This simulates the fact that there was + // nothing behind the primary split's tasks. + for (int i = secondaryChildren.size() - 1; i >= 0; --i) { + final ActivityManager.RunningTaskInfo ti = secondaryChildren.get(i); + if (isHomeOrRecentTask(ti)) { + wct.reparent(ti.token, null /* parent */, true /* onTop */); + // reset bounds and mode too + wct.setBounds(ti.token, null); + wct.setWindowingMode(ti.token, WINDOWING_MODE_UNDEFINED); + } + } + for (int i = primaryChildren.size() - 1; i >= 0; --i) { + wct.reparent(primaryChildren.get(i).token, null /* parent */, + true /* onTop */); + } + } + for (int i = freeHomeAndRecents.size() - 1; i >= 0; --i) { + wct.setBounds(freeHomeAndRecents.get(i).token, null); + wct.setWindowingMode(freeHomeAndRecents.get(i).token, WINDOWING_MODE_UNDEFINED); + } + // Reset focusable to true + wct.setFocusable(tiles.mPrimary.token, true /* focusable */); + applySyncTransaction(wct); + } + + /** + * Utility to apply a sync transaction serially with other sync transactions. + * + * @see SyncTransactionQueue#queue + */ + void applySyncTransaction(WindowContainerTransaction wct) { + mSyncTransactionQueue.queue(wct); + } + + /** + * @see SyncTransactionQueue#queueIfWaiting + */ + boolean queueSyncTransactionIfWaiting(WindowContainerTransaction wct) { + return mSyncTransactionQueue.queueIfWaiting(wct); + } + + /** + * @see SyncTransactionQueue#runInSync + */ + void runInSync(SyncTransactionQueue.TransactionRunnable runnable) { + mSyncTransactionQueue.runInSync(runnable); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java index 10672c8d87ad..497b6b714281 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java @@ -20,10 +20,14 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; import android.app.ActivityManager.RunningTaskInfo; -import android.content.res.Configuration; +import android.os.RemoteException; import android.view.SurfaceControl; +import android.window.ITaskOrganizer; +import android.window.ITaskOrganizerController; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -31,6 +35,8 @@ import androidx.test.filters.SmallTest; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import java.util.ArrayList; @@ -41,6 +47,9 @@ import java.util.ArrayList; @RunWith(AndroidJUnit4.class) public class ShellTaskOrganizerTests { + @Mock + private ITaskOrganizerController mTaskOrganizerController; + ShellTaskOrganizer mOrganizer; private class TrackingTaskListener implements ShellTaskOrganizer.TaskListener { @@ -71,7 +80,15 @@ public class ShellTaskOrganizerTests { @Before public void setUp() { - mOrganizer = new ShellTaskOrganizer(); + MockitoAnnotations.initMocks(this); + mOrganizer = new ShellTaskOrganizer(mTaskOrganizerController); + } + + @Test + public void registerOrganizer_sendRegisterTaskOrganizer() throws RemoteException { + mOrganizer.registerOrganizer(); + + verify(mTaskOrganizerController).registerTaskOrganizer(any(ITaskOrganizer.class)); } @Test diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index 76ec078ce3c9..4dbce92ed01c 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -84,7 +84,6 @@ VulkanManager::~VulkanManager() { mGraphicsQueue = VK_NULL_HANDLE; mAHBUploadQueue = VK_NULL_HANDLE; - mPresentQueue = VK_NULL_HANDLE; mDevice = VK_NULL_HANDLE; mPhysicalDevice = VK_NULL_HANDLE; mInstance = VK_NULL_HANDLE; @@ -192,10 +191,6 @@ void VulkanManager::setupDevice(GrVkExtensions& grExtensions, VkPhysicalDeviceFe } LOG_ALWAYS_FATAL_IF(mGraphicsQueueIndex == queueCount); - // All physical devices and queue families on Android must be capable of - // presentation with any native window. So just use the first one. - mPresentQueueIndex = 0; - { uint32_t extensionCount = 0; err = mEnumerateDeviceExtensionProperties(mPhysicalDevice, nullptr, &extensionCount, @@ -289,31 +284,21 @@ void VulkanManager::setupDevice(GrVkExtensions& grExtensions, VkPhysicalDeviceFe queueNextPtr = &queuePriorityCreateInfo; } - const VkDeviceQueueCreateInfo queueInfo[2] = { - { - VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, // sType - queueNextPtr, // pNext - 0, // VkDeviceQueueCreateFlags - mGraphicsQueueIndex, // queueFamilyIndex - 2, // queueCount - queuePriorities, // pQueuePriorities - }, - { - VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, // sType - queueNextPtr, // pNext - 0, // VkDeviceQueueCreateFlags - mPresentQueueIndex, // queueFamilyIndex - 1, // queueCount - queuePriorities, // pQueuePriorities - }}; - uint32_t queueInfoCount = (mPresentQueueIndex != mGraphicsQueueIndex) ? 2 : 1; + const VkDeviceQueueCreateInfo queueInfo = { + VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, // sType + queueNextPtr, // pNext + 0, // VkDeviceQueueCreateFlags + mGraphicsQueueIndex, // queueFamilyIndex + 2, // queueCount + queuePriorities, // pQueuePriorities + }; const VkDeviceCreateInfo deviceInfo = { VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, // sType &features, // pNext 0, // VkDeviceCreateFlags - queueInfoCount, // queueCreateInfoCount - queueInfo, // pQueueCreateInfos + 1, // queueCreateInfoCount + &queueInfo, // pQueueCreateInfos 0, // layerCount nullptr, // ppEnabledLayerNames (uint32_t)mDeviceExtensions.size(), // extensionCount @@ -361,8 +346,6 @@ void VulkanManager::initialize() { mGetDeviceQueue(mDevice, mGraphicsQueueIndex, 0, &mGraphicsQueue); mGetDeviceQueue(mDevice, mGraphicsQueueIndex, 1, &mAHBUploadQueue); - mGetDeviceQueue(mDevice, mPresentQueueIndex, 0, &mPresentQueue); - if (Properties::enablePartialUpdates && Properties::useBufferAge) { mSwapBehavior = SwapBehavior::BufferAge; } @@ -555,8 +538,8 @@ void VulkanManager::swapBuffers(VulkanSurface* surface, const SkRect& dirtyRect) void VulkanManager::destroySurface(VulkanSurface* surface) { // Make sure all submit commands have finished before starting to destroy objects. - if (VK_NULL_HANDLE != mPresentQueue) { - mQueueWaitIdle(mPresentQueue); + if (VK_NULL_HANDLE != mGraphicsQueue) { + mQueueWaitIdle(mGraphicsQueue); } mDeviceWaitIdle(mDevice); diff --git a/libs/hwui/renderthread/VulkanManager.h b/libs/hwui/renderthread/VulkanManager.h index 75c05b828e5d..7a77466303cd 100644 --- a/libs/hwui/renderthread/VulkanManager.h +++ b/libs/hwui/renderthread/VulkanManager.h @@ -163,8 +163,6 @@ private: uint32_t mGraphicsQueueIndex; VkQueue mGraphicsQueue = VK_NULL_HANDLE; VkQueue mAHBUploadQueue = VK_NULL_HANDLE; - uint32_t mPresentQueueIndex; - VkQueue mPresentQueue = VK_NULL_HANDLE; // Variables saved to populate VkFunctorInitParams. static const uint32_t mAPIVersion = VK_MAKE_VERSION(1, 1, 0); diff --git a/libs/hwui/shader/BlurShader.cpp b/libs/hwui/shader/BlurShader.cpp index 4d18cdd27e4e..fa10be100bca 100644 --- a/libs/hwui/shader/BlurShader.cpp +++ b/libs/hwui/shader/BlurShader.cpp @@ -26,7 +26,9 @@ BlurShader::BlurShader(float radiusX, float radiusY, Shader* inputShader, const SkImageFilters::Blur( Blur::convertRadiusToSigma(radiusX), Blur::convertRadiusToSigma(radiusY), - inputShader ? inputShader->asSkImageFilter() : nullptr) + SkTileMode::kClamp, + inputShader ? inputShader->asSkImageFilter() : nullptr, + nullptr) ) { } sk_sp<SkImageFilter> BlurShader::makeSkImageFilter() { |