summaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/WindowManager/Shell/Android.bp89
-rw-r--r--libs/WindowManager/Shell/proto/wm_shell_trace.proto27
-rw-r--r--libs/WindowManager/Shell/res/anim/forced_resizable_enter.xml21
-rw-r--r--libs/WindowManager/Shell/res/anim/forced_resizable_exit.xml22
-rw-r--r--libs/WindowManager/Shell/res/layout/divider.xml21
-rw-r--r--libs/WindowManager/Shell/res/layout/docked_stack_divider.xml38
-rw-r--r--libs/WindowManager/Shell/res/layout/forced_resizable_activity.xml26
-rw-r--r--libs/WindowManager/Shell/res/layout/pip_menu.xml (renamed from libs/WindowManager/Shell/res/layout/pip_menu_activity.xml)0
-rw-r--r--libs/WindowManager/Shell/res/raw/wm_shell_protolog.json46
-rw-r--r--libs/WindowManager/Shell/res/values-land/dimens.xml21
-rw-r--r--libs/WindowManager/Shell/res/values-land/styles.xml35
-rw-r--r--libs/WindowManager/Shell/res/values-sw600dp/config.xml25
-rw-r--r--libs/WindowManager/Shell/res/values/colors.xml25
-rw-r--r--libs/WindowManager/Shell/res/values/config.xml3
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml6
-rw-r--r--libs/WindowManager/Shell/res/values/ids.xml7
-rw-r--r--libs/WindowManager/Shell/res/values/strings.xml35
-rw-r--r--libs/WindowManager/Shell/res/values/styles.xml49
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java31
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/animation/FlingAnimationUtils.java423
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java45
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java176
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java92
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java138
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerHandleView.java146
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerImeController.java415
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerState.java25
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerView.java1374
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerWindowManager.java115
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ForcedResizableInfoActivity.java108
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ForcedResizableInfoActivityController.java148
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MinimizedDockShadow.java99
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitDisplayLayout.java313
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java91
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java546
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTaskOrganizer.java232
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/WindowManagerProxy.java369
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java21
-rw-r--r--libs/hwui/renderthread/VulkanManager.cpp41
-rw-r--r--libs/hwui/renderthread/VulkanManager.h2
-rw-r--r--libs/hwui/shader/BlurShader.cpp4
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() {