diff options
Diffstat (limited to 'libs')
338 files changed, 33767 insertions, 1943 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SettingsSidecarImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SettingsSidecarImpl.java index 92e575804bbe..5397302f6882 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SettingsSidecarImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SettingsSidecarImpl.java @@ -37,6 +37,8 @@ import android.util.Log; import androidx.annotation.NonNull; +import com.android.internal.R; + import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; @@ -151,13 +153,18 @@ class SettingsSidecarImpl extends StubSidecar { return features; } - ContentResolver resolver = mContext.getContentResolver(); - final String displayFeaturesString = Settings.Global.getString(resolver, DISPLAY_FEATURES); if (isInMultiWindow(windowToken)) { // It is recommended not to report any display features in multi-window mode, since it // won't be possible to synchronize the display feature positions with window movement. return features; } + + ContentResolver resolver = mContext.getContentResolver(); + String displayFeaturesString = Settings.Global.getString(resolver, DISPLAY_FEATURES); + if (TextUtils.isEmpty(displayFeaturesString)) { + displayFeaturesString = mContext.getResources().getString( + R.string.config_display_features); + } if (TextUtils.isEmpty(displayFeaturesString)) { return features; } @@ -192,7 +199,7 @@ class SettingsSidecarImpl extends StubSidecar { Rect featureRect = new Rect(left, top, right, bottom); rotateRectToDisplayRotation(featureRect, displayId); transformToWindowSpaceRect(featureRect, windowToken); - if (!featureRect.isEmpty()) { + if (isNotZero(featureRect)) { SidecarDisplayFeature feature = new SidecarDisplayFeature(); feature.setRect(featureRect); feature.setType(type); @@ -207,6 +214,10 @@ class SettingsSidecarImpl extends StubSidecar { return features; } + private static boolean isNotZero(Rect rect) { + return rect.height() > 0 || rect.width() > 0; + } + @Override protected void onListenersChanged() { if (mSettingsObserver == null) { diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index b8934dc8c583..0defbd6451fe 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -12,18 +12,119 @@ // 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", +} + +// TODO(b/168581922) protologtool do not support kotlin(*.kt) +filegroup { + name: "wm_shell-sources-kt", + srcs: [ + "src/**/*.kt", + ], + 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", + // TODO(b/168581922) protologtool do not support kotlin(*.kt) + ":wm_shell-sources-kt", "src/**/I*.aidl", ], resource_dirs: [ "res", ], + static_libs: [ + "androidx.dynamicanimation_dynamicanimation", + "kotlinx-coroutines-android", + "kotlinx-coroutines-core", + "protolog-lib", + "WindowManager-Shell-proto", + "androidx.appcompat_appcompat", + ], + kotlincflags: ["-Xjvm-default=enable"], manifest: "AndroidManifest.xml", - platform_apis: true, - sdk_version: "current", - min_sdk_version: "system_current", -} + min_sdk_version: "26", +}
\ 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/anim/tv_pip_controls_focus_gain_animation.xml b/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_gain_animation.xml new file mode 100644 index 000000000000..29d9b257cc59 --- /dev/null +++ b/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_gain_animation.xml @@ -0,0 +1,20 @@ +<?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. +--> +<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" + android:propertyName="alpha" + android:valueTo="1" + android:interpolator="@android:interpolator/fast_out_slow_in" + android:duration="100" /> diff --git a/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_loss_animation.xml b/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_loss_animation.xml new file mode 100644 index 000000000000..70f553b89657 --- /dev/null +++ b/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_loss_animation.xml @@ -0,0 +1,20 @@ +<?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. +--> +<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" + android:propertyName="alpha" + android:valueTo="0" + android:interpolator="@android:interpolator/fast_out_slow_in" + android:duration="100" /> diff --git a/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_in_animation.xml b/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_in_animation.xml new file mode 100644 index 000000000000..29d9b257cc59 --- /dev/null +++ b/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_in_animation.xml @@ -0,0 +1,20 @@ +<?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. +--> +<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" + android:propertyName="alpha" + android:valueTo="1" + android:interpolator="@android:interpolator/fast_out_slow_in" + android:duration="100" /> diff --git a/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_out_animation.xml b/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_out_animation.xml new file mode 100644 index 000000000000..70f553b89657 --- /dev/null +++ b/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_out_animation.xml @@ -0,0 +1,20 @@ +<?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. +--> +<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" + android:propertyName="alpha" + android:valueTo="0" + android:interpolator="@android:interpolator/fast_out_slow_in" + android:duration="100" /> diff --git a/libs/WindowManager/Shell/res/drawable-hdpi/one_handed_tutorial.png b/libs/WindowManager/Shell/res/drawable-hdpi/one_handed_tutorial.png Binary files differnew file mode 100644 index 000000000000..6c1f1cfdea7c --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable-hdpi/one_handed_tutorial.png diff --git a/libs/WindowManager/Shell/res/drawable/dismiss_circle_background.xml b/libs/WindowManager/Shell/res/drawable/dismiss_circle_background.xml new file mode 100644 index 000000000000..7809c8398c2d --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/dismiss_circle_background.xml @@ -0,0 +1,28 @@ +<?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. + --> + +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="oval"> + + <stroke + android:width="1dp" + android:color="#AAFFFFFF" /> + + <solid android:color="#77000000" /> + +</shape>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/floating_dismiss_gradient.xml b/libs/WindowManager/Shell/res/drawable/floating_dismiss_gradient.xml new file mode 100644 index 000000000000..8b3057d5841e --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/floating_dismiss_gradient.xml @@ -0,0 +1,25 @@ +<?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. +--> +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <gradient + android:angle="270" + android:startColor="#00000000" + android:endColor="#77000000" + android:type="linear" /> +</shape>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/floating_dismiss_gradient_transition.xml b/libs/WindowManager/Shell/res/drawable/floating_dismiss_gradient_transition.xml new file mode 100644 index 000000000000..772d0a5ea89b --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/floating_dismiss_gradient_transition.xml @@ -0,0 +1,20 @@ +<?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. +--> +<transition xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:drawable="@android:color/transparent" /> + <item android:drawable="@drawable/floating_dismiss_gradient" /> +</transition>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/pip_expand.xml b/libs/WindowManager/Shell/res/drawable/pip_expand.xml new file mode 100644 index 000000000000..c99d81934aab --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_expand.xml @@ -0,0 +1,28 @@ +<?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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + + <path + android:pathData="M0 0h36v36H0z" /> + <path + android:fillColor="#FFFFFF" + android:pathData="M10 21H7v8h8v-3h-5v-5zm-3-6h3v-5h5V7H7v8zm19 11h-5v3h8v-8h-3v5zM21 +7v3h5v5h3V7h-8z" /> +</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_close_white.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_close_white.xml new file mode 100644 index 000000000000..bcc850a854de --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_ic_close_white.xml @@ -0,0 +1,25 @@ +<?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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24.0dp" + android:height="24.0dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:pathData="M19.000000,6.400000l-1.400000,-1.400000 -5.600000,5.600000 -5.600000,-5.600000 -1.400000,1.400000 5.600000,5.600000 -5.600000,5.600000 1.400000,1.400000 5.600000,-5.600000 5.600000,5.600000 1.400000,-1.400000 -5.600000,-5.600000z" + android:fillColor="#FFFFFFFF"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_fullscreen_white.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_fullscreen_white.xml new file mode 100644 index 000000000000..56699dc04e10 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_ic_fullscreen_white.xml @@ -0,0 +1,25 @@ +<?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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="#FFFFFF" + android:pathData="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" /> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_pause_white.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_pause_white.xml new file mode 100644 index 000000000000..ef9b2d9c1c63 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_ic_pause_white.xml @@ -0,0 +1,26 @@ +<?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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + + <path + android:fillColor="#FFFFFF" + android:pathData="M6 19h4V5H6v14zm8-14v14h4V5h-4z" /> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_play_arrow_white.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_play_arrow_white.xml new file mode 100644 index 000000000000..f12d2cbebc87 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_ic_play_arrow_white.xml @@ -0,0 +1,26 @@ +<?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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + + <path + android:fillColor="#FFFFFF" + android:pathData="M8 5v14l11-7z" /> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_settings.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_settings.xml new file mode 100644 index 000000000000..b61e98ce2f9f --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_ic_settings.xml @@ -0,0 +1,28 @@ +<?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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M13.85,22.25h-3.7c-0.74,0 -1.36,-0.54 -1.45,-1.27l-0.27,-1.89c-0.27,-0.14 -0.53,-0.29 -0.79,-0.46l-1.8,0.72c-0.7,0.26 -1.47,-0.03 -1.81,-0.65L2.2,15.53c-0.35,-0.66 -0.2,-1.44 0.36,-1.88l1.53,-1.19c-0.01,-0.15 -0.02,-0.3 -0.02,-0.46c0,-0.15 0.01,-0.31 0.02,-0.46l-1.52,-1.19C1.98,9.9 1.83,9.09 2.2,8.47l1.85,-3.19c0.34,-0.62 1.11,-0.9 1.79,-0.63l1.81,0.73c0.26,-0.17 0.52,-0.32 0.78,-0.46l0.27,-1.91c0.09,-0.7 0.71,-1.25 1.44,-1.25h3.7c0.74,0 1.36,0.54 1.45,1.27l0.27,1.89c0.27,0.14 0.53,0.29 0.79,0.46l1.8,-0.72c0.71,-0.26 1.48,0.03 1.82,0.65l1.84,3.18c0.36,0.66 0.2,1.44 -0.36,1.88l-1.52,1.19c0.01,0.15 0.02,0.3 0.02,0.46s-0.01,0.31 -0.02,0.46l1.52,1.19c0.56,0.45 0.72,1.23 0.37,1.86l-1.86,3.22c-0.34,0.62 -1.11,0.9 -1.8,0.63l-1.8,-0.72c-0.26,0.17 -0.52,0.32 -0.78,0.46l-0.27,1.91C15.21,21.71 14.59,22.25 13.85,22.25zM13.32,20.72c0,0.01 0,0.01 0,0.02L13.32,20.72zM10.68,20.7l0,0.02C10.69,20.72 10.69,20.71 10.68,20.7zM10.62,20.25h2.76l0.37,-2.55l0.53,-0.22c0.44,-0.18 0.88,-0.44 1.34,-0.78l0.45,-0.34l2.38,0.96l1.38,-2.4l-2.03,-1.58l0.07,-0.56c0.03,-0.26 0.06,-0.51 0.06,-0.78c0,-0.27 -0.03,-0.53 -0.06,-0.78l-0.07,-0.56l2.03,-1.58l-1.39,-2.4l-2.39,0.96l-0.45,-0.35c-0.42,-0.32 -0.87,-0.58 -1.33,-0.77L13.75,6.3l-0.37,-2.55h-2.76L10.25,6.3L9.72,6.51C9.28,6.7 8.84,6.95 8.38,7.3L7.93,7.63L5.55,6.68L4.16,9.07l2.03,1.58l-0.07,0.56C6.09,11.47 6.06,11.74 6.06,12c0,0.26 0.02,0.53 0.06,0.78l0.07,0.56l-2.03,1.58l1.38,2.4l2.39,-0.96l0.45,0.35c0.43,0.33 0.86,0.58 1.33,0.77l0.53,0.22L10.62,20.25zM18.22,17.72c0,0.01 -0.01,0.02 -0.01,0.03L18.22,17.72zM5.77,17.71l0.01,0.02C5.78,17.72 5.77,17.71 5.77,17.71zM3.93,9.47L3.93,9.47C3.93,9.47 3.93,9.47 3.93,9.47zM18.22,6.27c0,0.01 0.01,0.02 0.01,0.02L18.22,6.27zM5.79,6.25L5.78,6.27C5.78,6.27 5.79,6.26 5.79,6.25zM13.31,3.28c0,0.01 0,0.01 0,0.02L13.31,3.28zM10.69,3.26l0,0.02C10.69,3.27 10.69,3.27 10.69,3.26z"/> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M12,12m-3.5,0a3.5,3.5 0,1 1,7 0a3.5,3.5 0,1 1,-7 0"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_skip_next_white.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_skip_next_white.xml new file mode 100644 index 000000000000..040c7e642241 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_ic_skip_next_white.xml @@ -0,0 +1,28 @@ +<?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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + + <path + android:fillColor="#FFFFFF" + android:pathData="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z" /> + <path + android:pathData="M0 0h24v24H0z" /> +</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_skip_previous_white.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_skip_previous_white.xml new file mode 100644 index 000000000000..b9b94b73a00f --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_ic_skip_previous_white.xml @@ -0,0 +1,28 @@ +<?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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + + <path + android:fillColor="#FFFFFF" + android:pathData="M6 6h2v12H6zm3.5 6l8.5 6V6z" /> + <path + android:pathData="M0 0h24v24H0z" /> +</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/pip_icon.xml b/libs/WindowManager/Shell/res/drawable/pip_icon.xml new file mode 100644 index 000000000000..b19d907d1ff3 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_icon.xml @@ -0,0 +1,25 @@ +<?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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="25" + android:viewportHeight="25"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M19,7h-8v6h8L19,7zM21,3L3,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,1.98 2,1.98h18c1.1,0 2,-0.88 2,-1.98L23,5c0,-1.1 -0.9,-2 -2,-2zM21,19.01L3,19.01L3,4.98h18v14.03z"/> +</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/pip_resize_handle.xml b/libs/WindowManager/Shell/res/drawable/pip_resize_handle.xml new file mode 100644 index 000000000000..4d1e080cf466 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_resize_handle.xml @@ -0,0 +1,29 @@ +<?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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="12.0dp" + android:height="12.0dp" + android:viewportWidth="12" + android:viewportHeight="12"> + <group + android:translateX="12" + android:rotation="90"> + <path + android:fillColor="#FFFFFF" + android:pathData="M3.41421 0L2 1.41422L10.4853 9.8995L11.8995 8.48528L3.41421 0ZM2.41421 4.24268L1 5.65689L6.65685 11.3137L8.07107 9.89953L2.41421 4.24268Z" /> + </group> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/tv_pip_button_focused.xml b/libs/WindowManager/Shell/res/drawable/tv_pip_button_focused.xml new file mode 100644 index 000000000000..cce13035dba7 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/tv_pip_button_focused.xml @@ -0,0 +1,18 @@ +<?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. +--> +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="#9AFFFFFF" android:radius="17dp" /> 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/one_handed_tutorial.xml b/libs/WindowManager/Shell/res/layout/one_handed_tutorial.xml new file mode 100644 index 000000000000..dc54caf0f14a --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/one_handed_tutorial.xml @@ -0,0 +1,67 @@ +<?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 + --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/one_handed_tutorial_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="center_horizontal | center_vertical" + android:background="@android:color/transparent"> + + <ImageView + android:id="@+id/one_handed_tutorial_image" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + android:layout_marginBottom="0dp" + android:gravity="center_horizontal" + android:src="@drawable/one_handed_tutorial" + android:scaleType="centerInside" /> + + <TextView + android:id="@+id/one_handed_tutorial_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + android:layout_marginBottom="0dp" + android:gravity="center_horizontal" + android:textAlignment="center" + android:fontFamily="google-sans-medium" + android:text="@string/one_handed_tutorial_title" + android:textSize="16sp" + android:textStyle="bold" + android:textColor="@android:color/white"/> + + <TextView + android:id="@+id/one_handed_tutorial_description" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + android:layout_marginBottom="0dp" + android:layout_marginStart="86dp" + android:layout_marginEnd="86dp" + android:gravity="center_horizontal" + android:fontFamily="roboto-regular" + android:text="@string/one_handed_tutorial_description" + android:textAlignment="center" + android:textSize="14sp" + android:textStyle="normal" + android:alpha="0.7" + android:textColor="@android:color/white"/> +</LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/pip_menu.xml b/libs/WindowManager/Shell/res/layout/pip_menu.xml new file mode 100644 index 000000000000..2e0a5e09e34f --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/pip_menu.xml @@ -0,0 +1,100 @@ +<?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. +--> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/background" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <!-- Menu layout --> + <FrameLayout + android:id="@+id/menu_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:forceHasOverlappingRendering="false" + android:accessibilityTraversalAfter="@id/dismiss"> + + <!-- The margins for this container is calculated in the code depending on whether the + actions_container is visible. --> + <FrameLayout + android:id="@+id/expand_container" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <ImageButton + android:id="@+id/expand_button" + android:layout_width="60dp" + android:layout_height="60dp" + android:layout_gravity="center" + android:contentDescription="@string/pip_phone_expand" + android:padding="10dp" + android:src="@drawable/pip_expand" + android:background="?android:selectableItemBackgroundBorderless" /> + </FrameLayout> + + <FrameLayout + android:id="@+id/actions_container" + android:layout_width="match_parent" + android:layout_height="@dimen/pip_action_size" + android:layout_gravity="bottom" + android:visibility="invisible"> + <LinearLayout + android:id="@+id/actions_group" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_gravity="center_horizontal" + android:orientation="horizontal" + android:divider="@android:color/transparent" + android:showDividers="middle" /> + </FrameLayout> + </FrameLayout> + + <LinearLayout + android:id="@+id/top_end_container" + android:layout_gravity="top|end" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal"> + <ImageButton + android:id="@+id/settings" + android:layout_width="@dimen/pip_action_size" + android:layout_height="@dimen/pip_action_size" + android:padding="@dimen/pip_action_padding" + android:contentDescription="@string/pip_phone_settings" + android:src="@drawable/pip_ic_settings" + android:background="?android:selectableItemBackgroundBorderless" /> + + <ImageButton + android:id="@+id/dismiss" + android:layout_width="@dimen/pip_action_size" + android:layout_height="@dimen/pip_action_size" + android:padding="@dimen/pip_action_padding" + android:contentDescription="@string/pip_phone_close" + android:src="@drawable/pip_ic_close_white" + android:background="?android:selectableItemBackgroundBorderless" /> + </LinearLayout> + + <!--TODO (b/156917828): Add content description for a11y purposes?--> + <ImageButton + android:id="@+id/resize_handle" + android:layout_width="@dimen/pip_resize_handle_size" + android:layout_height="@dimen/pip_resize_handle_size" + android:layout_gravity="top|start" + android:layout_margin="@dimen/pip_resize_handle_margin" + android:padding="@dimen/pip_resize_handle_padding" + android:src="@drawable/pip_resize_handle" + android:background="?android:selectableItemBackgroundBorderless" /> +</FrameLayout> diff --git a/libs/WindowManager/Shell/res/layout/pip_menu_action.xml b/libs/WindowManager/Shell/res/layout/pip_menu_action.xml new file mode 100644 index 000000000000..7a026ca63f50 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/pip_menu_action.xml @@ -0,0 +1,23 @@ +<?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. +--> +<ImageButton + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="@dimen/pip_action_size" + android:layout_height="@dimen/pip_action_size" + android:padding="@dimen/pip_action_padding" + android:background="?android:selectableItemBackgroundBorderless" + android:forceHasOverlappingRendering="false" /> diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_control_button.xml b/libs/WindowManager/Shell/res/layout/tv_pip_control_button.xml new file mode 100644 index 000000000000..727ac3412a25 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/tv_pip_control_button.xml @@ -0,0 +1,50 @@ +<?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. +--> +<!-- Layout for {@link com.android.wm.shell.pip.tv.PipControlButtonView}. --> +<merge xmlns:android="http://schemas.android.com/apk/res/android"> + + <ImageView android:id="@+id/button" + android:layout_width="34dp" + android:layout_height="34dp" + android:layout_alignParentTop="true" + android:layout_centerHorizontal="true" + android:focusable="true" + android:src="@drawable/tv_pip_button_focused" + android:importantForAccessibility="yes" /> + + <ImageView android:id="@+id/icon" + android:layout_width="34dp" + android:layout_height="34dp" + android:layout_alignParentTop="true" + android:layout_centerHorizontal="true" + android:padding="5dp" + android:importantForAccessibility="no" /> + + <TextView android:id="@+id/desc" + android:layout_width="100dp" + android:layout_height="wrap_content" + android:layout_below="@id/icon" + android:layout_centerHorizontal="true" + android:layout_marginTop="3dp" + android:gravity="center" + android:text="@string/pip_fullscreen" + android:alpha="0" + android:fontFamily="sans-serif" + android:textSize="12sp" + android:textColor="#EEEEEE" + android:importantForAccessibility="no" /> +</merge> diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_controls.xml b/libs/WindowManager/Shell/res/layout/tv_pip_controls.xml new file mode 100644 index 000000000000..d2f235e273d5 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/tv_pip_controls.xml @@ -0,0 +1,43 @@ +<?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. +--> +<!-- Layout for {@link com.android.wm.shell.pip.tv.PipControlsView}. --> +<merge xmlns:android="http://schemas.android.com/apk/res/android"> + + <com.android.wm.shell.pip.tv.PipControlButtonView + android:id="@+id/full_button" + android:layout_width="@dimen/picture_in_picture_button_width" + android:layout_height="wrap_content" + android:src="@drawable/pip_ic_fullscreen_white" + android:text="@string/pip_fullscreen" /> + + <com.android.wm.shell.pip.tv.PipControlButtonView + android:id="@+id/close_button" + android:layout_width="@dimen/picture_in_picture_button_width" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/picture_in_picture_button_start_margin" + android:src="@drawable/pip_ic_close_white" + android:text="@string/pip_close" /> + + <com.android.wm.shell.pip.tv.PipControlButtonView + android:id="@+id/play_pause_button" + android:layout_width="@dimen/picture_in_picture_button_width" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/picture_in_picture_button_start_margin" + android:src="@drawable/pip_ic_pause_white" + android:text="@string/pip_pause" + android:visibility="gone" /> +</merge> diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_custom_control.xml b/libs/WindowManager/Shell/res/layout/tv_pip_custom_control.xml new file mode 100644 index 000000000000..452f2cd5ccb6 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/tv_pip_custom_control.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. +--> +<com.android.wm.shell.pip.tv.PipControlButtonView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="@dimen/picture_in_picture_button_width" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/picture_in_picture_button_start_margin" /> diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml new file mode 100644 index 000000000000..d8474b865a36 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml @@ -0,0 +1,31 @@ +<?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. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="horizontal" + android:paddingTop="350dp" + android:background="#CC000000" + android:gravity="top|center_horizontal" + android:clipChildren="false"> + + <com.android.wm.shell.pip.tv.PipControlsView + android:id="@+id/pip_controls" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:alpha="0" /> +</LinearLayout> 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..3f6ca0fc5246 --- /dev/null +++ b/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json @@ -0,0 +1,97 @@ +{ + "version": "1.0.0", + "messages": { + "-1683614271": { + "message": "Existing task: id=%d component=%s", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" + }, + "-1534364071": { + "message": "onTransitionReady %s: %s", + "level": "VERBOSE", + "group": "WM_SHELL_TRANSITIONS", + "at": "com\/android\/wm\/shell\/Transitions.java" + }, + "-1501874464": { + "message": "Fullscreen Task Appeared: #%d", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/FullscreenTaskListener.java" + }, + "-1480787369": { + "message": "Transition requested: type=%d %s", + "level": "VERBOSE", + "group": "WM_SHELL_TRANSITIONS", + "at": "com\/android\/wm\/shell\/Transitions.java" + }, + "-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" + }, + "-191422040": { + "message": "Transition animations finished, notifying core %s", + "level": "VERBOSE", + "group": "WM_SHELL_TRANSITIONS", + "at": "com\/android\/wm\/shell\/Transitions.java" + }, + "157713005": { + "message": "Task info changed taskId=%d", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" + }, + "481673835": { + "message": "addListenerForTaskId taskId=%s", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" + }, + "564235578": { + "message": "Fullscreen Task Vanished: #%d", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/FullscreenTaskListener.java" + }, + "580605218": { + "message": "Registering organizer", + "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" + }, + "1990759023": { + "message": "addListenerForType types=%s listener=%s", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" + } + }, + "groups": { + "WM_SHELL_TASK_ORG": { + "tag": "WindowManagerShell" + }, + "WM_SHELL_TRANSITIONS": { + "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-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml new file mode 100644 index 000000000000..7920fd237a08 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml @@ -0,0 +1,22 @@ +<?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> + <!-- The dimensions to user for picture-in-picture action buttons. --> + <dimen name="picture_in_picture_button_width">100dp</dimen> + <dimen name="picture_in_picture_button_start_margin">-50dp</dimen> +</resources> + 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 c894eb0133b5..e99350b264b9 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -1,21 +1,39 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -/* -** Copyright 2019, 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. -*/ ---> + 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> -</resources>
\ No newline at end of file + <!-- Animation duration for resizing of PIP when entering/exiting. --> + <integer name="config_pipResizeAnimationDuration">425</integer> + + <!-- Allow dragging the PIP to a location to close it --> + <bool name="config_pipEnableDismissDragToEdge">true</bool> + + <!-- Allow PIP to resize to a slightly bigger state upon touch/showing the menu --> + <bool name="config_pipEnableResizeForMenu">true</bool> + + <!-- 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> + + <!-- Allow one handed to enable round corner --> + <bool name="config_one_handed_enable_round_corner">true</bool> + + <!-- Bounds [left top right bottom] on screen for picture-in-picture (PIP) windows, + when the PIP menu is shown in center. --> + <string translatable="false" name="pip_menu_bounds">"596 280 1324 690"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml new file mode 100644 index 000000000000..a9917a6b07da --- /dev/null +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -0,0 +1,72 @@ +<?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="dismiss_circle_size">52dp</dimen> + + <!-- The height of the gradient indicating the dismiss edge when moving a PIP. --> + <dimen name="floating_dismiss_gradient_height">250dp</dimen> + + <!-- The padding around a PiP actions. --> + <dimen name="pip_action_padding">12dp</dimen> + + <!-- The height of the PiP actions container in which the actions are vertically centered. --> + <dimen name="pip_action_size">48dp</dimen> + + <!-- The padding between actions in the PiP in landscape Note that the PiP does not reflect + the configuration of the device, so we can't use -land resources. --> + <dimen name="pip_between_action_padding_land">8dp</dimen> + + <!-- The buffer to use when calculating whether the pip is in an adjust zone. --> + <dimen name="pip_bottom_offset_buffer">1dp</dimen> + + <!-- The corner radius for PiP window. --> + <dimen name="pip_corner_radius">8dp</dimen> + + <!-- The bottom margin of the PIP drag to dismiss info text shown when moving a PIP. --> + <dimen name="pip_dismiss_text_bottom_margin">24dp</dimen> + + <!-- The bottom margin of the expand container when there are actions. + Equal to pip_action_size - pip_action_padding. --> + <dimen name="pip_expand_container_edge_margin">30dp</dimen> + + <!-- The shortest-edge size of the expanded PiP. --> + <dimen name="pip_expanded_shortest_edge_size">160dp</dimen> + + <!-- The additional offset to apply to the IME animation to account for the input field. --> + <dimen name="pip_ime_offset">48dp</dimen> + + <!-- The touchable/draggable edge size for PIP resize. --> + <dimen name="pip_resize_edge_size">48dp</dimen> + + <!-- PIP Resize handle size, margin and padding. --> + <dimen name="pip_resize_handle_size">12dp</dimen> + <dimen name="pip_resize_handle_margin">4dp</dimen> + <dimen name="pip_resize_handle_padding">0dp</dimen> + + <dimen name="dismiss_target_x_size">24dp</dimen> + <dimen name="floating_dismiss_bottom_margin">50dp</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> + + <!-- One-Handed Mode --> + <!-- Threshold for dragging distance to enable one-handed mode --> + <dimen name="gestures_onehanded_drag_threshold">20dp</dimen> +</resources> diff --git a/libs/WindowManager/Shell/res/values/ids.xml b/libs/WindowManager/Shell/res/values/ids.xml new file mode 100644 index 000000000000..fb892388cf74 --- /dev/null +++ b/libs/WindowManager/Shell/res/values/ids.xml @@ -0,0 +1,26 @@ +<?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> + <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 new file mode 100644 index 000000000000..da5965dab71a --- /dev/null +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -0,0 +1,100 @@ +<?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:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Label for PIP close button [CHAR LIMIT=NONE]--> + <string name="pip_phone_close">Close</string> + + <!-- Making the PIP fullscreen [CHAR LIMIT=25] --> + <string name="pip_phone_expand">Expand</string> + + <!-- Label for PIP settings button [CHAR LIMIT=NONE]--> + <string name="pip_phone_settings">Settings</string> + + <!-- Title of menu shown over picture-in-picture. Used for accessibility. --> + <string name="pip_menu_title">Menu</string> + + <!-- PiP BTW notification title. [CHAR LIMIT=50] --> + <string name="pip_notification_title"><xliff:g id="name" example="Google Maps">%s</xliff:g> is in picture-in-picture</string> + + <!-- PiP BTW notification description. [CHAR LIMIT=NONE] --> + <string name="pip_notification_message">If you don\'t want <xliff:g id="name" example="Google Maps">%s</xliff:g> to use this feature, tap to open settings and turn it off.</string> + + <!-- Button to play the current media on picture-in-picture (PIP) [CHAR LIMIT=30] --> + <string name="pip_play">Play</string> + + <!-- Button to pause the current media on picture-in-picture (PIP) [CHAR LIMIT=30] --> + <string name="pip_pause">Pause</string> + + <!-- Button to skip to the next media on picture-in-picture (PIP) [CHAR LIMIT=30] --> + <string name="pip_skip_to_next">Skip to next</string> + + <!-- Button to skip to the prev media on picture-in-picture (PIP) [CHAR LIMIT=30] --> + <string name="pip_skip_to_prev">Skip to previous</string> + + <!-- Accessibility action for resizing PIP [CHAR LIMIT=NONE] --> + <string name="accessibility_action_pip_resize">Resize</string> + + <!-- TODO Deprecated. Label for PIP action to Minimize the PIP. DO NOT TRANSLATE [CHAR LIMIT=25] --> + <string name="pip_phone_minimize">Minimize</string> + + <!-- 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> + + <!-- One-Handed Tutorial title [CHAR LIMIT=60] --> + <string name="one_handed_tutorial_title">Using one-handed mode</string> + <!-- One-Handed Tutorial description [CHAR LIMIT=NONE] --> + <string name="one_handed_tutorial_description">To exit, swipe up from the bottom of the screen or tap anywhere above the app</string> + <!-- Accessibility description for start one-handed mode [CHAR LIMIT=NONE] --> + <string name="accessibility_action_start_one_handed">Start one-handed mode</string> + <!-- Accessibility description for stop one-handed mode [CHAR LIMIT=NONE] --> + <string name="accessibility_action_stop_one_handed">Exit one-handed mode</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values/strings_tv.xml b/libs/WindowManager/Shell/res/values/strings_tv.xml new file mode 100644 index 000000000000..2dfdcabaa931 --- /dev/null +++ b/libs/WindowManager/Shell/res/values/strings_tv.xml @@ -0,0 +1,34 @@ +<?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:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Picture-in-Picture (PIP) notification --> + <!-- Title for the notification channel for TV PIP controls. [CHAR LIMIT=NONE] --> + <string name="notification_channel_tv_pip">Picture-in-Picture</string> + + <!-- Title of the picture-in-picture (PIP) notification title + when the media doesn't have title [CHAR LIMIT=NONE] --> + <string name="pip_notification_unknown_title">(No title program)</string> + + <!-- Picture-in-Picture (PIP) menu --> + <eat-comment /> + <!-- Button to close picture-in-picture (PIP) in PIP menu [CHAR LIMIT=30] --> + <string name="pip_close">Close PIP</string> + + <!-- Button to move picture-in-picture (PIP) screen to the fullscreen in PIP menu [CHAR LIMIT=30] --> + <string name="pip_fullscreen">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/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java new file mode 100644 index 000000000000..5bd693a9311e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java @@ -0,0 +1,123 @@ +/* + * 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; + +import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN; +import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString; + +import android.app.ActivityManager; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.util.ArrayMap; +import android.util.Slog; +import android.view.SurfaceControl; + +import androidx.annotation.NonNull; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.io.PrintWriter; + +class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { + private static final String TAG = "FullscreenTaskListener"; + + private final SyncTransactionQueue mSyncQueue; + + private final ArrayMap<Integer, SurfaceControl> mTasks = new ArrayMap<>(); + + FullscreenTaskListener(SyncTransactionQueue syncQueue) { + mSyncQueue = syncQueue; + } + + @Override + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + synchronized (mTasks) { + if (mTasks.containsKey(taskInfo.taskId)) { + throw new RuntimeException("Task appeared more than once: #" + taskInfo.taskId); + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Appeared: #%d", + taskInfo.taskId); + mTasks.put(taskInfo.taskId, leash); + mSyncQueue.runInSync(t -> { + // Reset several properties back to fullscreen (PiP, for example, leaves all these + // properties in a bad state). + updateSurfacePosition(t, taskInfo, leash); + t.setWindowCrop(leash, null); + // TODO(shell-transitions): Eventually set everything in transition so there's no + // SF Transaction here. + if (!Transitions.ENABLE_SHELL_TRANSITIONS) { + t.setAlpha(leash, 1f); + t.setMatrix(leash, 1, 0, 0, 1); + t.show(leash); + } + }); + } + } + + @Override + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + synchronized (mTasks) { + if (mTasks.remove(taskInfo.taskId) == null) { + Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId); + return; + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Vanished: #%d", + taskInfo.taskId); + } + } + + @Override + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + synchronized (mTasks) { + if (!mTasks.containsKey(taskInfo.taskId)) { + Slog.e(TAG, "Changed Task wasn't appeared or already vanished: #" + + taskInfo.taskId); + return; + } + final SurfaceControl leash = mTasks.get(taskInfo.taskId); + mSyncQueue.runInSync(t -> { + // Reposition the task in case the bounds has been changed (such as Task level + // letterboxing). + updateSurfacePosition(t, taskInfo, leash); + }); + } + } + + @Override + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + final String childPrefix = innerPrefix + " "; + pw.println(prefix + this); + pw.println(innerPrefix + mTasks.size() + " Tasks"); + } + + @Override + public String toString() { + return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_FULLSCREEN); + } + + /** Places the Task surface to the latest position. */ + private static void updateSurfacePosition(SurfaceControl.Transaction t, + ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + // TODO(170725334) drop this after ag/12876439 + final Configuration config = taskInfo.getConfiguration(); + final Rect bounds = config.windowConfiguration.getBounds(); + t.setPosition(leash, bounds.left, bounds.top); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java new file mode 100644 index 000000000000..cbc1c8d6d310 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -0,0 +1,409 @@ +/* + * 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; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; +import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; + +import android.annotation.IntDef; +import android.app.ActivityManager.RunningTaskInfo; +import android.app.WindowConfiguration.WindowingMode; +import android.os.IBinder; +import android.util.ArrayMap; +import android.util.Log; +import android.util.SparseArray; +import android.view.SurfaceControl; +import android.window.ITaskOrganizerController; +import android.window.TaskAppearedInfo; +import android.window.TaskOrganizer; + +import androidx.annotation.NonNull; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Unified task organizer for all components in the shell. + * TODO(b/167582004): may consider consolidating this class and TaskOrganizer + */ +public class ShellTaskOrganizer extends TaskOrganizer { + + // Intentionally using negative numbers here so the positive numbers can be used + // for task id specific listeners that will be added later. + public static final int TASK_LISTENER_TYPE_UNDEFINED = -1; + public static final int TASK_LISTENER_TYPE_FULLSCREEN = -2; + public static final int TASK_LISTENER_TYPE_MULTI_WINDOW = -3; + public static final int TASK_LISTENER_TYPE_PIP = -4; + public static final int TASK_LISTENER_TYPE_SPLIT_SCREEN = -5; + + @IntDef(prefix = {"TASK_LISTENER_TYPE_"}, value = { + TASK_LISTENER_TYPE_UNDEFINED, + TASK_LISTENER_TYPE_FULLSCREEN, + TASK_LISTENER_TYPE_MULTI_WINDOW, + TASK_LISTENER_TYPE_PIP, + TASK_LISTENER_TYPE_SPLIT_SCREEN, + }) + public @interface TaskListenerType {} + + private static final String TAG = "ShellTaskOrganizer"; + + /** + * Callbacks for when the tasks change in the system. + */ + public interface TaskListener { + default void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {} + default void onTaskInfoChanged(RunningTaskInfo taskInfo) {} + default void onTaskVanished(RunningTaskInfo taskInfo) {} + default void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) {} + default void dump(@NonNull PrintWriter pw, String prefix) {}; + } + + /** + * Keys map from either a task id or {@link TaskListenerType}. + * @see #addListenerForTaskId + * @see #addListenerForType + */ + private final SparseArray<TaskListener> mTaskListeners = new SparseArray<>(); + + // Keeps track of all the tasks reported to this organizer (changes in windowing mode will + // require us to report to both old and new listeners) + private final SparseArray<TaskAppearedInfo> mTasks = new SparseArray<>(); + + /** @see #setPendingLaunchCookieListener */ + private final ArrayMap<IBinder, TaskListener> mLaunchCookieToListener = new ArrayMap<>(); + + // TODO(shell-transitions): move to a more "global" Shell location as this isn't only for Tasks + private final Transitions mTransitions; + + private final Object mLock = new Object(); + + public ShellTaskOrganizer(SyncTransactionQueue syncQueue, TransactionPool transactionPool, + ShellExecutor mainExecutor, ShellExecutor animExecutor) { + this(null, syncQueue, transactionPool, mainExecutor, animExecutor); + } + + @VisibleForTesting + ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController, + SyncTransactionQueue syncQueue, TransactionPool transactionPool, + ShellExecutor mainExecutor, ShellExecutor animExecutor) { + super(taskOrganizerController, mainExecutor); + addListenerForType(new FullscreenTaskListener(syncQueue), TASK_LISTENER_TYPE_FULLSCREEN); + mTransitions = new Transitions(this, transactionPool, mainExecutor, animExecutor); + if (Transitions.ENABLE_SHELL_TRANSITIONS) registerTransitionPlayer(mTransitions); + } + + @Override + public List<TaskAppearedInfo> registerOrganizer() { + synchronized (mLock) { + ProtoLog.v(WM_SHELL_TASK_ORG, "Registering organizer"); + final List<TaskAppearedInfo> taskInfos = super.registerOrganizer(); + for (int i = 0; i < taskInfos.size(); i++) { + final TaskAppearedInfo info = taskInfos.get(i); + ProtoLog.v(WM_SHELL_TASK_ORG, "Existing task: id=%d component=%s", + info.getTaskInfo().taskId, info.getTaskInfo().baseIntent); + onTaskAppeared(info); + } + return taskInfos; + } + } + + /** + * Adds a listener for a specific task id. + */ + public void addListenerForTaskId(TaskListener listener, int taskId) { + synchronized (mLock) { + ProtoLog.v(WM_SHELL_TASK_ORG, "addListenerForTaskId taskId=%s", taskId); + if (mTaskListeners.get(taskId) != null) { + throw new IllegalArgumentException( + "Listener for taskId=" + taskId + " already exists"); + } + + final TaskAppearedInfo info = mTasks.get(taskId); + if (info == null) { + throw new IllegalArgumentException("addListenerForTaskId unknown taskId=" + taskId); + } + + final TaskListener oldListener = getTaskListener(info.getTaskInfo()); + mTaskListeners.put(taskId, listener); + updateTaskListenerIfNeeded(info.getTaskInfo(), info.getLeash(), oldListener, listener); + } + } + + /** + * Adds a listener for tasks with given types. + */ + public void addListenerForType(TaskListener listener, @TaskListenerType int... listenerTypes) { + synchronized (mLock) { + ProtoLog.v(WM_SHELL_TASK_ORG, "addListenerForType types=%s listener=%s", + Arrays.toString(listenerTypes), listener); + for (int listenerType : listenerTypes) { + if (mTaskListeners.get(listenerType) != null) { + throw new IllegalArgumentException("Listener for listenerType=" + listenerType + + " already exists"); + } + mTaskListeners.put(listenerType, listener); + + // Notify the listener of all existing tasks with the given type. + for (int i = mTasks.size() - 1; i >= 0; --i) { + final TaskAppearedInfo data = mTasks.valueAt(i); + final TaskListener taskListener = getTaskListener(data.getTaskInfo()); + if (taskListener != listener) continue; + listener.onTaskAppeared(data.getTaskInfo(), data.getLeash()); + } + } + } + } + + /** + * Removes a registered listener. + */ + public void removeListener(TaskListener listener) { + synchronized (mLock) { + ProtoLog.v(WM_SHELL_TASK_ORG, "Remove listener=%s", listener); + final int index = mTaskListeners.indexOfValue(listener); + if (index == -1) { + Log.w(TAG, "No registered listener found"); + return; + } + + // Collect tasks associated with the listener we are about to remove. + final ArrayList<TaskAppearedInfo> tasks = new ArrayList<>(); + for (int i = mTasks.size() - 1; i >= 0; --i) { + final TaskAppearedInfo data = mTasks.valueAt(i); + final TaskListener taskListener = getTaskListener(data.getTaskInfo()); + if (taskListener != listener) continue; + tasks.add(data); + } + + // Remove listener + mTaskListeners.removeAt(index); + + // Associate tasks with new listeners if needed. + for (int i = tasks.size() - 1; i >= 0; --i) { + final TaskAppearedInfo data = tasks.get(i); + updateTaskListenerIfNeeded(data.getTaskInfo(), data.getLeash(), + null /* oldListener already removed*/, getTaskListener(data.getTaskInfo())); + } + } + } + + /** + * Associated a listener to a pending launch cookie so we can route the task later once it + * appears. + */ + public void setPendingLaunchCookieListener(IBinder cookie, TaskListener listener) { + synchronized (mLock) { + mLaunchCookieToListener.put(cookie, listener); + } + } + + @Override + public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { + synchronized (mLock) { + onTaskAppeared(new TaskAppearedInfo(taskInfo, leash)); + } + } + + private void onTaskAppeared(TaskAppearedInfo info) { + final int taskId = info.getTaskInfo().taskId; + ProtoLog.v(WM_SHELL_TASK_ORG, "Task appeared taskId=%d", taskId); + mTasks.put(taskId, info); + final TaskListener listener = + getTaskListener(info.getTaskInfo(), true /*removeLaunchCookieIfNeeded*/); + if (listener != null) { + listener.onTaskAppeared(info.getTaskInfo(), info.getLeash()); + } + } + + @Override + public void onTaskInfoChanged(RunningTaskInfo taskInfo) { + synchronized (mLock) { + ProtoLog.v(WM_SHELL_TASK_ORG, "Task info changed taskId=%d", taskInfo.taskId); + final TaskAppearedInfo data = mTasks.get(taskInfo.taskId); + final TaskListener oldListener = getTaskListener(data.getTaskInfo()); + final TaskListener newListener = getTaskListener(taskInfo); + mTasks.put(taskInfo.taskId, new TaskAppearedInfo(taskInfo, data.getLeash())); + final boolean updated = updateTaskListenerIfNeeded( + taskInfo, data.getLeash(), oldListener, newListener); + if (!updated && newListener != null) { + newListener.onTaskInfoChanged(taskInfo); + } + } + } + + @Override + public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) { + synchronized (mLock) { + ProtoLog.v(WM_SHELL_TASK_ORG, "Task root back pressed taskId=%d", taskInfo.taskId); + final TaskListener listener = getTaskListener(taskInfo); + if (listener != null) { + listener.onBackPressedOnTaskRoot(taskInfo); + } + } + } + + @Override + public void onTaskVanished(RunningTaskInfo taskInfo) { + synchronized (mLock) { + ProtoLog.v(WM_SHELL_TASK_ORG, "Task vanished taskId=%d", taskInfo.taskId); + final int taskId = taskInfo.taskId; + final TaskListener listener = getTaskListener(mTasks.get(taskId).getTaskInfo()); + mTasks.remove(taskId); + if (listener != null) { + listener.onTaskVanished(taskInfo); + } + } + } + + private boolean updateTaskListenerIfNeeded(RunningTaskInfo taskInfo, SurfaceControl leash, + TaskListener oldListener, TaskListener newListener) { + if (oldListener == newListener) return false; + // TODO: We currently send vanished/appeared as the task moves between types, but + // we should consider adding a different mode-changed callback + if (oldListener != null) { + oldListener.onTaskVanished(taskInfo); + } + if (newListener != null) { + newListener.onTaskAppeared(taskInfo, leash); + } + return true; + } + + private TaskListener getTaskListener(RunningTaskInfo runningTaskInfo) { + return getTaskListener(runningTaskInfo, false /*removeLaunchCookieIfNeeded*/); + } + + private TaskListener getTaskListener(RunningTaskInfo runningTaskInfo, + boolean removeLaunchCookieIfNeeded) { + + final int taskId = runningTaskInfo.taskId; + TaskListener listener; + + // First priority goes to listener that might be pending for this task. + final ArrayList<IBinder> launchCookies = runningTaskInfo.launchCookies; + for (int i = launchCookies.size() - 1; i >= 0; --i) { + final IBinder cookie = launchCookies.get(i); + listener = mLaunchCookieToListener.get(cookie); + if (listener == null) continue; + + if (removeLaunchCookieIfNeeded) { + // Remove the cookie and add the listener. + mLaunchCookieToListener.remove(cookie); + mTaskListeners.put(taskId, listener); + } + return listener; + } + + // Next priority goes to taskId specific listeners. + listener = mTaskListeners.get(taskId); + if (listener != null) return listener; + + // Next we try type specific listeners. + final int windowingMode = getWindowingMode(runningTaskInfo); + final int taskListenerType = windowingModeToTaskListenerType(windowingMode); + return mTaskListeners.get(taskListenerType); + } + + @WindowingMode + private static int getWindowingMode(RunningTaskInfo taskInfo) { + return taskInfo.configuration.windowConfiguration.getWindowingMode(); + } + + private static @TaskListenerType int windowingModeToTaskListenerType( + @WindowingMode int windowingMode) { + switch (windowingMode) { + case WINDOWING_MODE_FULLSCREEN: + return TASK_LISTENER_TYPE_FULLSCREEN; + case WINDOWING_MODE_MULTI_WINDOW: + return TASK_LISTENER_TYPE_MULTI_WINDOW; + case WINDOWING_MODE_SPLIT_SCREEN_PRIMARY: + case WINDOWING_MODE_SPLIT_SCREEN_SECONDARY: + return TASK_LISTENER_TYPE_SPLIT_SCREEN; + case WINDOWING_MODE_PINNED: + return TASK_LISTENER_TYPE_PIP; + case WINDOWING_MODE_FREEFORM: + case WINDOWING_MODE_UNDEFINED: + default: + return TASK_LISTENER_TYPE_UNDEFINED; + } + } + + public static String taskListenerTypeToString(@TaskListenerType int type) { + switch (type) { + case TASK_LISTENER_TYPE_FULLSCREEN: + return "TASK_LISTENER_TYPE_FULLSCREEN"; + case TASK_LISTENER_TYPE_MULTI_WINDOW: + return "TASK_LISTENER_TYPE_MULTI_WINDOW"; + case TASK_LISTENER_TYPE_SPLIT_SCREEN: + return "TASK_LISTENER_TYPE_SPLIT_SCREEN"; + case TASK_LISTENER_TYPE_PIP: + return "TASK_LISTENER_TYPE_PIP"; + case TASK_LISTENER_TYPE_UNDEFINED: + return "TASK_LISTENER_TYPE_UNDEFINED"; + default: + return "taskId#" + type; + } + } + + public void dump(@NonNull PrintWriter pw, String prefix) { + synchronized (mLock) { + final String innerPrefix = prefix + " "; + final String childPrefix = innerPrefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + mTaskListeners.size() + " Listeners"); + for (int i = mTaskListeners.size() - 1; i >= 0; --i) { + final int key = mTaskListeners.keyAt(i); + final TaskListener listener = mTaskListeners.valueAt(i); + pw.println(innerPrefix + "#" + i + " " + taskListenerTypeToString(key)); + listener.dump(pw, childPrefix); + } + + pw.println(); + pw.println(innerPrefix + mTasks.size() + " Tasks"); + for (int i = mTasks.size() - 1; i >= 0; --i) { + final int key = mTasks.keyAt(i); + final TaskAppearedInfo info = mTasks.valueAt(i); + final TaskListener listener = getTaskListener(info.getTaskInfo()); + pw.println(innerPrefix + "#" + i + " task=" + key + " listener=" + listener); + } + + pw.println(); + pw.println(innerPrefix + mLaunchCookieToListener.size() + " Launch Cookies"); + for (int i = mLaunchCookieToListener.size() - 1; i >= 0; --i) { + final IBinder key = mLaunchCookieToListener.keyAt(i); + final TaskListener listener = mLaunchCookieToListener.valueAt(i); + pw.println(innerPrefix + "#" + i + " cookie=" + key + " listener=" + listener); + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/Transitions.java new file mode 100644 index 000000000000..04be3b70fc65 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/Transitions.java @@ -0,0 +1,181 @@ +/* + * 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; + +import static android.window.TransitionInfo.TRANSIT_CLOSE; +import static android.window.TransitionInfo.TRANSIT_HIDE; +import static android.window.TransitionInfo.TRANSIT_OPEN; +import static android.window.TransitionInfo.TRANSIT_SHOW; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.annotation.MainThread; +import android.annotation.NonNull; +import android.os.IBinder; +import android.os.SystemProperties; +import android.util.ArrayMap; +import android.util.Slog; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.ITransitionPlayer; +import android.window.TransitionInfo; +import android.window.WindowOrganizer; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.util.ArrayList; + +/** Plays transition animations */ +public class Transitions extends ITransitionPlayer.Stub { + private static final String TAG = "ShellTransitions"; + + /** Set to {@code true} to enable shell transitions. */ + public static final boolean ENABLE_SHELL_TRANSITIONS = + SystemProperties.getBoolean("persist.debug.shell_transit", false); + + private final WindowOrganizer mOrganizer; + private final TransactionPool mTransactionPool; + private final ShellExecutor mMainExecutor; + private final ShellExecutor mAnimExecutor; + + /** Keeps track of currently tracked transitions and all the animations associated with each */ + private final ArrayMap<IBinder, ArrayList<Animator>> mActiveTransitions = new ArrayMap<>(); + + Transitions(@NonNull WindowOrganizer organizer, @NonNull TransactionPool pool, + @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { + mOrganizer = organizer; + mTransactionPool = pool; + mMainExecutor = mainExecutor; + mAnimExecutor = animExecutor; + } + + // TODO(shell-transitions): real animations + private void startExampleAnimation(@NonNull IBinder transition, @NonNull SurfaceControl leash, + boolean show) { + final float end = show ? 1.f : 0.f; + final float start = 1.f - end; + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + final ValueAnimator va = ValueAnimator.ofFloat(start, end); + va.setDuration(500); + va.addUpdateListener(animation -> { + float fraction = animation.getAnimatedFraction(); + transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction); + transaction.apply(); + }); + final Runnable finisher = () -> { + transaction.setAlpha(leash, end); + transaction.apply(); + mTransactionPool.release(transaction); + mMainExecutor.execute(() -> { + mActiveTransitions.get(transition).remove(va); + onFinish(transition); + }); + }; + va.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { } + + @Override + public void onAnimationEnd(Animator animation) { + finisher.run(); + } + + @Override + public void onAnimationCancel(Animator animation) { + finisher.run(); + } + + @Override + public void onAnimationRepeat(Animator animation) { } + }); + mActiveTransitions.get(transition).add(va); + mAnimExecutor.execute(va::start); + } + + private static boolean isOpeningType(@WindowManager.TransitionOldType int legacyType) { + // TODO(shell-transitions): consider providing and using z-order vs the global type for + // this determination. + return legacyType == WindowManager.TRANSIT_OLD_TASK_OPEN + || legacyType == WindowManager.TRANSIT_OLD_TASK_TO_FRONT + || legacyType == WindowManager.TRANSIT_OLD_TASK_OPEN_BEHIND + || legacyType == WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY; + } + + @Override + public void onTransitionReady(@NonNull IBinder transitionToken, TransitionInfo info, + @NonNull SurfaceControl.Transaction t) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady %s: %s", + transitionToken, info); + // start task + mMainExecutor.execute(() -> { + if (!mActiveTransitions.containsKey(transitionToken)) { + Slog.e(TAG, "Got transitionReady for non-active transition " + transitionToken + + " expecting one of " + mActiveTransitions.keySet()); + } + if (mActiveTransitions.get(transitionToken) != null) { + throw new IllegalStateException("Got a duplicate onTransitionReady call for " + + transitionToken); + } + mActiveTransitions.put(transitionToken, new ArrayList<>()); + for (int i = 0; i < info.getChanges().size(); ++i) { + final SurfaceControl leash = info.getChanges().get(i).getLeash(); + final int mode = info.getChanges().get(i).getMode(); + if (mode == TRANSIT_OPEN || mode == TRANSIT_SHOW) { + t.show(leash); + t.setMatrix(leash, 1, 0, 0, 1); + if (isOpeningType(info.getType())) { + t.setAlpha(leash, 0.f); + startExampleAnimation(transitionToken, leash, true /* show */); + } else { + t.setAlpha(leash, 1.f); + } + } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_HIDE) { + if (!isOpeningType(info.getType())) { + startExampleAnimation(transitionToken, leash, false /* show */); + } + } + } + t.apply(); + onFinish(transitionToken); + }); + } + + @MainThread + private void onFinish(IBinder transition) { + if (!mActiveTransitions.get(transition).isEmpty()) return; + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "Transition animations finished, notifying core %s", transition); + mActiveTransitions.remove(transition); + mOrganizer.finishTransition(transition, null, null); + } + + @Override + public void requestStartTransition(int type, @NonNull IBinder transitionToken) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition requested: type=%d %s", + type, transitionToken); + mMainExecutor.execute(() -> { + if (mActiveTransitions.containsKey(transitionToken)) { + throw new RuntimeException("Transition already started " + transitionToken); + } + IBinder transition = mOrganizer.startTransition(type, transitionToken, null /* wct */); + mActiveTransitions.put(transition, null); + }); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/WindowManagerShellWrapper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/WindowManagerShellWrapper.java new file mode 100644 index 000000000000..acb9a5dae78c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/WindowManagerShellWrapper.java @@ -0,0 +1,63 @@ +/* + * 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; + +import static android.view.Display.DEFAULT_DISPLAY; + +import android.app.WindowConfiguration; +import android.os.RemoteException; +import android.view.WindowManagerGlobal; + +import com.android.wm.shell.pip.PinnedStackListenerForwarder; + +/** + * The singleton wrapper to communicate between WindowManagerService and WMShell features + * (e.g: PIP, SplitScreen, Bubble, OneHandedMode...etc) + */ +public class WindowManagerShellWrapper { + private static final String TAG = WindowManagerShellWrapper.class.getSimpleName(); + + public static final int WINDOWING_MODE_PINNED = WindowConfiguration.WINDOWING_MODE_PINNED; + + /** + * Forwarder to which we can add multiple pinned stack listeners. Each listener will receive + * updates from the window manager service. + */ + private PinnedStackListenerForwarder mPinnedStackListenerForwarder = + new PinnedStackListenerForwarder(); + + /** + * Adds a pinned stack listener, which will receive updates from the window manager service + * along with any other pinned stack listeners that were added via this method. + */ + public void addPinnedStackListener(PinnedStackListenerForwarder.PinnedStackListener listener) + throws + RemoteException { + mPinnedStackListenerForwarder.addListener(listener); + WindowManagerGlobal.getWindowManagerService().registerPinnedStackListener( + DEFAULT_DISPLAY, mPinnedStackListenerForwarder); + } + + /** + * Removes a pinned stack listener. + */ + public void removePinnedStackListener( + PinnedStackListenerForwarder.PinnedStackListener listener) { + mPinnedStackListenerForwarder.removeListener(listener); + } + +} 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/FloatProperties.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/FloatProperties.kt new file mode 100644 index 000000000000..d4f82829aa52 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/FloatProperties.kt @@ -0,0 +1,141 @@ +/* + * 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.graphics.Rect +import android.graphics.RectF +import androidx.dynamicanimation.animation.FloatPropertyCompat + +/** + * Helpful extra properties to use with the [PhysicsAnimator]. These allow you to animate objects + * such as [Rect] and [RectF]. + * + * There are additional, more basic properties available in [DynamicAnimation]. + */ +class FloatProperties { + companion object { + /** + * Represents the x-coordinate of a [Rect]. Typically used to animate moving a Rect + * horizontally. + * + * This property's getter returns [Rect.left], and its setter uses [Rect.offsetTo], which + * sets [Rect.left] to the new value and offsets [Rect.right] so that the width of the Rect + * does not change. + */ + @JvmField + val RECT_X = object : FloatPropertyCompat<Rect>("RectX") { + override fun setValue(rect: Rect?, value: Float) { + rect?.offsetTo(value.toInt(), rect.top) + } + + override fun getValue(rect: Rect?): Float { + return rect?.left?.toFloat() ?: -Float.MAX_VALUE + } + } + + /** + * Represents the y-coordinate of a [Rect]. Typically used to animate moving a Rect + * vertically. + * + * This property's getter returns [Rect.top], and its setter uses [Rect.offsetTo], which + * sets [Rect.top] to the new value and offsets [Rect.bottom] so that the height of the Rect + * does not change. + */ + @JvmField + val RECT_Y = object : FloatPropertyCompat<Rect>("RectY") { + override fun setValue(rect: Rect?, value: Float) { + rect?.offsetTo(rect.left, value.toInt()) + } + + override fun getValue(rect: Rect?): Float { + return rect?.top?.toFloat() ?: -Float.MAX_VALUE + } + } + + /** + * Represents the width of a [Rect]. Typically used to animate resizing a Rect horizontally. + * + * This property's getter returns [Rect.width], and its setter changes the value of + * [Rect.right] by adding the animated width value to [Rect.left]. + */ + @JvmField + val RECT_WIDTH = object : FloatPropertyCompat<Rect>("RectWidth") { + override fun getValue(rect: Rect): Float { + return rect.width().toFloat() + } + + override fun setValue(rect: Rect, value: Float) { + rect.right = rect.left + value.toInt() + } + } + + /** + * Represents the height of a [Rect]. Typically used to animate resizing a Rect vertically. + * + * This property's getter returns [Rect.height], and its setter changes the value of + * [Rect.bottom] by adding the animated height value to [Rect.top]. + */ + @JvmField + val RECT_HEIGHT = object : FloatPropertyCompat<Rect>("RectHeight") { + override fun getValue(rect: Rect): Float { + return rect.height().toFloat() + } + + override fun setValue(rect: Rect, value: Float) { + rect.bottom = rect.top + value.toInt() + } + } + + /** + * Represents the x-coordinate of a [RectF]. Typically used to animate moving a RectF + * horizontally. + * + * This property's getter returns [RectF.left], and its setter uses [RectF.offsetTo], which + * sets [RectF.left] to the new value and offsets [RectF.right] so that the width of the + * RectF does not change. + */ + @JvmField + val RECTF_X = object : FloatPropertyCompat<RectF>("RectFX") { + override fun setValue(rect: RectF?, value: Float) { + rect?.offsetTo(value, rect.top) + } + + override fun getValue(rect: RectF?): Float { + return rect?.left ?: -Float.MAX_VALUE + } + } + + /** + * Represents the y-coordinate of a [RectF]. Typically used to animate moving a RectF + * vertically. + * + * This property's getter returns [RectF.top], and its setter uses [RectF.offsetTo], which + * sets [RectF.top] to the new value and offsets [RectF.bottom] so that the height of the + * RectF does not change. + */ + @JvmField + val RECTF_Y = object : FloatPropertyCompat<RectF>("RectFY") { + override fun setValue(rect: RectF?, value: Float) { + rect?.offsetTo(rect.left, value) + } + + override fun getValue(rect: RectF?): Float { + return rect?.top ?: -Float.MAX_VALUE + } + } + } +}
\ No newline at end of file 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..416ada739aa3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java @@ -0,0 +1,55 @@ +/* + * 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 alpha in animation. + */ + public static final Interpolator ALPHA_IN = new PathInterpolator(0.4f, 0f, 1f, 1f); + + /** + * Interpolator for alpha out animation. + */ + public static final Interpolator ALPHA_OUT = new PathInterpolator(0f, 0f, 0.8f, 1f); + + /** + * 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/animation/PhysicsAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt new file mode 100644 index 000000000000..5cd660a2caa5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt @@ -0,0 +1,1071 @@ +/* + * 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.os.Looper +import android.util.ArrayMap +import android.util.Log +import android.view.View +import androidx.dynamicanimation.animation.AnimationHandler +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.FlingAnimation +import androidx.dynamicanimation.animation.FloatPropertyCompat +import androidx.dynamicanimation.animation.SpringAnimation +import androidx.dynamicanimation.animation.SpringForce +import com.android.wm.shell.animation.PhysicsAnimator.Companion.getInstance +import java.lang.ref.WeakReference +import java.util.WeakHashMap +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +/** + * Extension function for all objects which will return a PhysicsAnimator instance for that object. + */ +val <T : View> T.physicsAnimator: PhysicsAnimator<T> get() { return getInstance(this) } + +private const val TAG = "PhysicsAnimator" + +private val UNSET = -Float.MAX_VALUE + +/** + * [FlingAnimation] multiplies the friction set via [FlingAnimation.setFriction] by 4.2f, which is + * where this number comes from. We use it in [PhysicsAnimator.flingThenSpring] to calculate the + * minimum velocity for a fling to reach a certain value, given the fling's friction. + */ +private const val FLING_FRICTION_SCALAR_MULTIPLIER = 4.2f + +typealias EndAction = () -> Unit + +/** A map of Property -> AnimationUpdate, which is provided to update listeners on each frame. */ +typealias UpdateMap<T> = + ArrayMap<FloatPropertyCompat<in T>, PhysicsAnimator.AnimationUpdate> + +/** + * Map of the animators associated with a given object. This ensures that only one animator + * per object exists. + */ +internal val animators = WeakHashMap<Any, PhysicsAnimator<*>>() + +/** + * Default spring configuration to use for animations where stiffness and/or damping ratio + * were not provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig]. + */ +private val globalDefaultSpring = PhysicsAnimator.SpringConfig( + SpringForce.STIFFNESS_MEDIUM, + SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) + +/** + * Default fling configuration to use for animations where friction was not provided, and a default + * fling config was not set via [PhysicsAnimator.setDefaultFlingConfig]. + */ +private val globalDefaultFling = PhysicsAnimator.FlingConfig( + friction = 1f, min = -Float.MAX_VALUE, max = Float.MAX_VALUE) + +/** Whether to log helpful debug information about animations. */ +private var verboseLogging = false + +/** + * Animator that uses physics-based animations to animate properties on views and objects. Physics + * animations use real-world physical concepts, such as momentum and mass, to realistically simulate + * motion. PhysicsAnimator is heavily inspired by [android.view.ViewPropertyAnimator], and + * also uses the builder pattern to configure and start animations. + * + * The physics animations are backed by [DynamicAnimation]. + * + * @param T The type of the object being animated. + */ +class PhysicsAnimator<T> private constructor (target: T) { + /** Weak reference to the animation target. */ + val weakTarget = WeakReference(target) + + /** Data class for representing animation frame updates. */ + data class AnimationUpdate(val value: Float, val velocity: Float) + + /** [DynamicAnimation] instances for the given properties. */ + private val springAnimations = ArrayMap<FloatPropertyCompat<in T>, SpringAnimation>() + private val flingAnimations = ArrayMap<FloatPropertyCompat<in T>, FlingAnimation>() + + /** + * Spring and fling configurations for the properties to be animated on the target. We'll + * configure and start the DynamicAnimations for these properties according to the provided + * configurations. + */ + private val springConfigs = ArrayMap<FloatPropertyCompat<in T>, SpringConfig>() + private val flingConfigs = ArrayMap<FloatPropertyCompat<in T>, FlingConfig>() + + /** + * Animation listeners for the animation. These will be notified when each property animation + * updates or ends. + */ + private val updateListeners = ArrayList<UpdateListener<T>>() + private val endListeners = ArrayList<EndListener<T>>() + + /** End actions to run when all animations have completed. */ + private val endActions = ArrayList<EndAction>() + + /** SpringConfig to use by default for properties whose springs were not provided. */ + private var defaultSpring: SpringConfig = globalDefaultSpring + + /** FlingConfig to use by default for properties whose fling configs were not provided. */ + private var defaultFling: FlingConfig = globalDefaultFling + + /** + * AnimationHandler to use if it need custom AnimationHandler, if this is null, it will use + * the default AnimationHandler in the DynamicAnimation. + */ + private var customAnimationHandler: AnimationHandler? = null + + /** + * Internal listeners that respond to DynamicAnimations updating and ending, and dispatch to + * the listeners provided via [addUpdateListener] and [addEndListener]. This allows us to add + * just one permanent update and end listener to the DynamicAnimations. + */ + internal var internalListeners = ArrayList<InternalListener>() + + /** + * Action to run when [start] is called. This can be changed by + * [PhysicsAnimatorTestUtils.prepareForTest] to enable animators to run under test and provide + * helpful test utilities. + */ + internal var startAction: () -> Unit = ::startInternal + + /** + * Action to run when [cancel] is called. This can be changed by + * [PhysicsAnimatorTestUtils.prepareForTest] to cancel animations from the main thread, which + * is required. + */ + internal var cancelAction: (Set<FloatPropertyCompat<in T>>) -> Unit = ::cancelInternal + + /** + * Springs a property to the given value, using the provided configuration settings. + * + * Springs are used when you know the exact value to which you want to animate. They can be + * configured with a start velocity (typically used when the spring is initiated by a touch + * event), but this velocity will be realistically attenuated as forces are applied to move the + * property towards the end value. + * + * If you find yourself repeating the same stiffness and damping ratios many times, consider + * storing a single [SpringConfig] instance and passing that in instead of individual values. + * + * @param property The property to spring to the given value. The property must be an instance + * of FloatPropertyCompat<? super T>. For example, if this is a + * PhysicsAnimator<FrameLayout>, you can use a FloatPropertyCompat<FrameLayout>, as + * well as a FloatPropertyCompat<ViewGroup>, and so on. + * @param toPosition The value to spring the given property to. + * @param startVelocity The initial velocity to use for the animation. + * @param stiffness The stiffness to use for the spring. Higher stiffness values result in + * faster animations, while lower stiffness means a slower animation. Reasonable values for + * low, medium, and high stiffness can be found as constants in [SpringForce]. + * @param dampingRatio The damping ratio (bounciness) to use for the spring. Higher values + * result in a less 'springy' animation, while lower values allow the animation to bounce + * back and forth for a longer time after reaching the final position. Reasonable values for + * low, medium, and high damping can be found in [SpringForce]. + */ + fun spring( + property: FloatPropertyCompat<in T>, + toPosition: Float, + startVelocity: Float = 0f, + stiffness: Float = defaultSpring.stiffness, + dampingRatio: Float = defaultSpring.dampingRatio + ): PhysicsAnimator<T> { + if (verboseLogging) { + Log.d(TAG, "Springing ${getReadablePropertyName(property)} to $toPosition.") + } + + springConfigs[property] = + SpringConfig(stiffness, dampingRatio, startVelocity, toPosition) + return this + } + + /** + * Springs a property to a given value using the provided start velocity and configuration + * options. + * + * @see spring + */ + fun spring( + property: FloatPropertyCompat<in T>, + toPosition: Float, + startVelocity: Float, + config: SpringConfig = defaultSpring + ): PhysicsAnimator<T> { + return spring( + property, toPosition, startVelocity, config.stiffness, config.dampingRatio) + } + + /** + * Springs a property to a given value using the provided configuration options, and a start + * velocity of 0f. + * + * @see spring + */ + fun spring( + property: FloatPropertyCompat<in T>, + toPosition: Float, + config: SpringConfig = defaultSpring + ): PhysicsAnimator<T> { + return spring(property, toPosition, 0f, config) + } + + /** + * Springs a property to a given value using the provided configuration options, and a start + * velocity of 0f. + * + * @see spring + */ + fun spring( + property: FloatPropertyCompat<in T>, + toPosition: Float + ): PhysicsAnimator<T> { + return spring(property, toPosition, 0f) + } + + /** + * Flings a property using the given start velocity, using a [FlingAnimation] configured using + * the provided configuration settings. + * + * Flings are used when you have a start velocity, and want the property value to realistically + * decrease as friction is applied until the velocity reaches zero. Flings do not have a + * deterministic end value. If you are attempting to animate to a specific end value, use + * [spring]. + * + * If you find yourself repeating the same friction/min/max values, consider storing a single + * [FlingConfig] and passing that in instead. + * + * @param property The property to fling using the given start velocity. + * @param startVelocity The start velocity (in pixels per second) with which to start the fling. + * @param friction Friction value applied to slow down the animation over time. Higher values + * will more quickly slow the animation. Typical friction values range from 1f to 10f. + * @param min The minimum value allowed for the animation. If this value is reached, the + * animation will end abruptly. + * @param max The maximum value allowed for the animation. If this value is reached, the + * animation will end abruptly. + */ + fun fling( + property: FloatPropertyCompat<in T>, + startVelocity: Float, + friction: Float = defaultFling.friction, + min: Float = defaultFling.min, + max: Float = defaultFling.max + ): PhysicsAnimator<T> { + if (verboseLogging) { + Log.d(TAG, "Flinging ${getReadablePropertyName(property)} " + + "with velocity $startVelocity.") + } + + flingConfigs[property] = FlingConfig(friction, min, max, startVelocity) + return this + } + + /** + * Flings a property using the given start velocity, using a [FlingAnimation] configured using + * the provided configuration settings. + * + * @see fling + */ + fun fling( + property: FloatPropertyCompat<in T>, + startVelocity: Float, + config: FlingConfig = defaultFling + ): PhysicsAnimator<T> { + return fling(property, startVelocity, config.friction, config.min, config.max) + } + + /** + * Flings a property using the given start velocity. If the fling animation reaches the min/max + * bounds (from the [flingConfig]) with velocity remaining, it'll overshoot it and spring back. + * + * If the object is already out of the fling bounds, it will immediately spring back within + * bounds. + * + * This is useful for animating objects that are bounded by constraints such as screen edges, + * since otherwise the fling animation would end abruptly upon reaching the min/max bounds. + * + * @param property The property to animate. + * @param startVelocity The velocity, in pixels/second, with which to start the fling. If the + * object is already outside the fling bounds, this velocity will be used as the start velocity + * of the spring that will spring it back within bounds. + * @param flingMustReachMinOrMax If true, the fling animation is guaranteed to reach either its + * minimum bound (if [startVelocity] is negative) or maximum bound (if it's positive). The + * animator will use startVelocity if it's sufficient, or add more velocity if necessary. This + * is useful when fling's deceleration-based physics are preferable to the acceleration-based + * forces used by springs - typically, when you're allowing the user to move an object somewhere + * on the screen, but it needs to be along an edge. + * @param flingConfig The configuration to use for the fling portion of the animation. + * @param springConfig The configuration to use for the spring portion of the animation. + */ + @JvmOverloads + fun flingThenSpring( + property: FloatPropertyCompat<in T>, + startVelocity: Float, + flingConfig: FlingConfig, + springConfig: SpringConfig, + flingMustReachMinOrMax: Boolean = false + ): PhysicsAnimator<T> { + val target = weakTarget.get() + if (target == null) { + Log.w(TAG, "Trying to animate a GC-ed target.") + return this + } + val flingConfigCopy = flingConfig.copy() + val springConfigCopy = springConfig.copy() + val toAtLeast = if (startVelocity < 0) flingConfig.min else flingConfig.max + + if (flingMustReachMinOrMax && isValidValue(toAtLeast)) { + val currentValue = property.getValue(target) + val flingTravelDistance = + startVelocity / (flingConfig.friction * FLING_FRICTION_SCALAR_MULTIPLIER) + val projectedFlingEndValue = currentValue + flingTravelDistance + val midpoint = (flingConfig.min + flingConfig.max) / 2 + + // If fling velocity is too low to push the target past the midpoint between min and + // max, then spring back towards the nearest edge, starting with the current velocity. + if ((startVelocity < 0 && projectedFlingEndValue > midpoint) || + (startVelocity > 0 && projectedFlingEndValue < midpoint)) { + val toPosition = + if (projectedFlingEndValue < midpoint) flingConfig.min else flingConfig.max + if (isValidValue(toPosition)) { + return spring(property, toPosition, startVelocity, springConfig) + } + } + + // Projected fling end value is past the midpoint, so fling forward. + val distanceToDestination = toAtLeast - property.getValue(target) + + // The minimum velocity required for the fling to end up at the given destination, + // taking the provided fling friction value. + val velocityToReachDestination = distanceToDestination * + (flingConfig.friction * FLING_FRICTION_SCALAR_MULTIPLIER) + + // If there's distance to cover, and the provided velocity is moving in the correct + // direction, ensure that the velocity is high enough to reach the destination. + // Otherwise, just use startVelocity - this means that the fling is at or out of bounds. + // The fling will immediately end and a spring will bring the object back into bounds + // with this startVelocity. + flingConfigCopy.startVelocity = when { + distanceToDestination > 0f && startVelocity >= 0f -> + max(velocityToReachDestination, startVelocity) + distanceToDestination < 0f && startVelocity <= 0f -> + min(velocityToReachDestination, startVelocity) + else -> startVelocity + } + + springConfigCopy.finalPosition = toAtLeast + } else { + flingConfigCopy.startVelocity = startVelocity + } + + flingConfigs[property] = flingConfigCopy + springConfigs[property] = springConfigCopy + return this + } + + private fun isValidValue(value: Float) = value < Float.MAX_VALUE && value > -Float.MAX_VALUE + + /** + * Adds a listener that will be called whenever any property on the animated object is updated. + * This will be called on every animation frame, with the current value of the animated object + * and the new property values. + */ + fun addUpdateListener(listener: UpdateListener<T>): PhysicsAnimator<T> { + updateListeners.add(listener) + return this + } + + /** + * Adds a listener that will be called when a property stops animating. This is useful if + * you care about a specific property ending, or want to use the end value/end velocity from a + * particular property's animation. If you just want to run an action when all property + * animations have ended, use [withEndActions]. + */ + fun addEndListener(listener: EndListener<T>): PhysicsAnimator<T> { + endListeners.add(listener) + return this + } + + /** + * Adds end actions that will be run sequentially when animations for every property involved in + * this specific animation have ended (unless they were explicitly canceled). For example, if + * you call: + * + * animator + * .spring(TRANSLATION_X, ...) + * .spring(TRANSLATION_Y, ...) + * .withEndAction(action) + * .start() + * + * 'action' will be run when both TRANSLATION_X and TRANSLATION_Y end. + * + * Other properties may still be animating, if those animations were not started in the same + * call. For example: + * + * animator + * .spring(ALPHA, ...) + * .start() + * + * animator + * .spring(TRANSLATION_X, ...) + * .spring(TRANSLATION_Y, ...) + * .withEndAction(action) + * .start() + * + * 'action' will still be run as soon as TRANSLATION_X and TRANSLATION_Y end, even if ALPHA is + * still animating. + * + * If you want to run actions as soon as a subset of property animations have ended, you want + * access to the animation's end value/velocity, or you want to run these actions even if the + * animation is explicitly canceled, use [addEndListener]. End listeners have an allEnded param, + * which indicates that all relevant animations have ended. + */ + fun withEndActions(vararg endActions: EndAction?): PhysicsAnimator<T> { + this.endActions.addAll(endActions.filterNotNull()) + return this + } + + /** + * Helper overload so that callers from Java can use Runnables or method references as end + * actions without having to explicitly return Unit. + */ + fun withEndActions(vararg endActions: Runnable?): PhysicsAnimator<T> { + this.endActions.addAll(endActions.filterNotNull().map { it::run }) + return this + } + + fun setDefaultSpringConfig(defaultSpring: SpringConfig) { + this.defaultSpring = defaultSpring + } + + fun setDefaultFlingConfig(defaultFling: FlingConfig) { + this.defaultFling = defaultFling + } + + /** + * Set the custom AnimationHandler for all aniatmion in this animator. Set this with null for + * restoring to default AnimationHandler. + */ + fun setCustomAnimationHandler(handler: AnimationHandler) { + this.customAnimationHandler = handler + } + + /** Starts the animations! */ + fun start() { + startAction() + } + + /** + * Starts the animations for real! This is typically called immediately by [start] unless this + * animator is under test. + */ + internal fun startInternal() { + if (!Looper.getMainLooper().isCurrentThread) { + Log.e(TAG, "Animations can only be started on the main thread. If you are seeing " + + "this message in a test, call PhysicsAnimatorTestUtils#prepareForTest in " + + "your test setup.") + } + val target = weakTarget.get() + if (target == null) { + Log.w(TAG, "Trying to animate a GC-ed object.") + return + } + + // Functions that will actually start the animations. These are run after we build and add + // the InternalListener, since some animations might update/end immediately and we don't + // want to miss those updates. + val animationStartActions = ArrayList<() -> Unit>() + + for (animatedProperty in getAnimatedProperties()) { + val flingConfig = flingConfigs[animatedProperty] + val springConfig = springConfigs[animatedProperty] + + // The property's current value on the object. + val currentValue = animatedProperty.getValue(target) + + // Start by checking for a fling configuration. If one is present, we're either flinging + // or flinging-then-springing. Either way, we'll want to start the fling first. + if (flingConfig != null) { + animationStartActions.add { + // When the animation is starting, adjust the min/max bounds to include the + // current value of the property, if necessary. This is required to allow a + // fling to bring an out-of-bounds object back into bounds. For example, if an + // object was dragged halfway off the left side of the screen, but then flung to + // the right, we don't want the animation to end instantly just because the + // object started out of bounds. If the fling is in the direction that would + // take it farther out of bounds, it will end instantly as expected. + flingConfig.apply { + min = min(currentValue, this.min) + max = max(currentValue, this.max) + } + + // Flings can't be updated to a new position while maintaining velocity, because + // we're using the explicitly provided start velocity. Cancel any flings (or + // springs) on this property before flinging. + cancel(animatedProperty) + + // Apply the custom animation handler if it not null + val flingAnim = getFlingAnimation(animatedProperty, target) + flingAnim.animationHandler = + customAnimationHandler ?: flingAnim.animationHandler + + // Apply the configuration and start the animation. + flingAnim.also { flingConfig.applyToAnimation(it) }.start() + } + } + + // Check for a spring configuration. If one is present, we're either springing, or + // flinging-then-springing. + if (springConfig != null) { + + // If there is no corresponding fling config, we're only springing. + if (flingConfig == null) { + // Apply the configuration and start the animation. + val springAnim = getSpringAnimation(animatedProperty, target) + + // If customAnimationHander is exist and has not been set to the animation, + // it should set here. + if (customAnimationHandler != null && + springAnim.animationHandler != customAnimationHandler) { + // Cancel the animation before set animation handler + if (springAnim.isRunning) { + cancel(animatedProperty) + } + // Apply the custom animation handler if it not null + springAnim.animationHandler = + customAnimationHandler ?: springAnim.animationHandler + } + + // Apply the configuration and start the animation. + springConfig.applyToAnimation(springAnim) + animationStartActions.add(springAnim::start) + } else { + // If there's a corresponding fling config, we're flinging-then-springing. Save + // the fling's original bounds so we can spring to them when the fling ends. + val flingMin = flingConfig.min + val flingMax = flingConfig.max + + // Add an end listener that will start the spring when the fling ends. + endListeners.add(0, object : EndListener<T> { + override fun onAnimationEnd( + target: T, + property: FloatPropertyCompat<in T>, + wasFling: Boolean, + canceled: Boolean, + finalValue: Float, + finalVelocity: Float, + allRelevantPropertyAnimsEnded: Boolean + ) { + // If this isn't the relevant property, it wasn't a fling, or the fling + // was explicitly cancelled, don't spring. + if (property != animatedProperty || !wasFling || canceled) { + return + } + + val endedWithVelocity = abs(finalVelocity) > 0 + + // If the object was out of bounds when the fling animation started, it + // will immediately end. In that case, we'll spring it back in bounds. + val endedOutOfBounds = finalValue !in flingMin..flingMax + + // If the fling ended either out of bounds or with remaining velocity, + // it's time to spring. + if (endedWithVelocity || endedOutOfBounds) { + springConfig.startVelocity = finalVelocity + + // If the spring's final position isn't set, this is a + // flingThenSpring where flingMustReachMinOrMax was false. We'll + // need to set the spring's final position here. + if (springConfig.finalPosition == UNSET) { + if (endedWithVelocity) { + // If the fling ended with negative velocity, that means it + // hit the min bound, so spring to that bound (and vice + // versa). + springConfig.finalPosition = + if (finalVelocity < 0) flingMin else flingMax + } else if (endedOutOfBounds) { + // If the fling ended out of bounds, spring it to the + // nearest bound. + springConfig.finalPosition = + if (finalValue < flingMin) flingMin else flingMax + } + } + + // Apply the custom animation handler if it not null + val springAnim = getSpringAnimation(animatedProperty, target) + springAnim.animationHandler = + customAnimationHandler ?: springAnim.animationHandler + + // Apply the configuration and start the spring animation. + springAnim.also { springConfig.applyToAnimation(it) }.start() + } + } + }) + } + } + } + + // Add an internal listener that will dispatch animation events to the provided listeners. + internalListeners.add(InternalListener( + target, + getAnimatedProperties(), + ArrayList(updateListeners), + ArrayList(endListeners), + ArrayList(endActions))) + + // Actually start the DynamicAnimations. This is delayed until after the InternalListener is + // constructed and added so that we don't miss the end listener firing for any animations + // that immediately end. + animationStartActions.forEach { it.invoke() } + + clearAnimator() + } + + /** Clear the animator's builder variables. */ + private fun clearAnimator() { + springConfigs.clear() + flingConfigs.clear() + + updateListeners.clear() + endListeners.clear() + endActions.clear() + } + + /** Retrieves a spring animation for the given property, building one if needed. */ + private fun getSpringAnimation( + property: FloatPropertyCompat<in T>, + target: T + ): SpringAnimation { + return springAnimations.getOrPut( + property, + { configureDynamicAnimation(SpringAnimation(target, property), property) + as SpringAnimation }) + } + + /** Retrieves a fling animation for the given property, building one if needed. */ + private fun getFlingAnimation(property: FloatPropertyCompat<in T>, target: T): FlingAnimation { + return flingAnimations.getOrPut( + property, + { configureDynamicAnimation(FlingAnimation(target, property), property) + as FlingAnimation }) + } + + /** + * Adds update and end listeners to the DynamicAnimation which will dispatch to the internal + * listeners. + */ + private fun configureDynamicAnimation( + anim: DynamicAnimation<*>, + property: FloatPropertyCompat<in T> + ): DynamicAnimation<*> { + anim.addUpdateListener { _, value, velocity -> + for (i in 0 until internalListeners.size) { + internalListeners[i].onInternalAnimationUpdate(property, value, velocity) + } + } + anim.addEndListener { _, canceled, value, velocity -> + internalListeners.removeAll { + it.onInternalAnimationEnd( + property, canceled, value, velocity, anim is FlingAnimation) + } + if (springAnimations[property] == anim) { + springAnimations.remove(property) + } + if (flingAnimations[property] == anim) { + flingAnimations.remove(property) + } + } + return anim + } + + /** + * Internal listener class that receives updates from DynamicAnimation listeners, and dispatches + * them to the appropriate update/end listeners. This class is also aware of which properties + * were being animated when the end listeners were passed in, so that we can provide the + * appropriate value for allEnded to [EndListener.onAnimationEnd]. + */ + internal inner class InternalListener constructor( + private val target: T, + private var properties: Set<FloatPropertyCompat<in T>>, + private var updateListeners: List<UpdateListener<T>>, + private var endListeners: List<EndListener<T>>, + private var endActions: List<EndAction> + ) { + + /** The number of properties whose animations haven't ended. */ + private var numPropertiesAnimating = properties.size + + /** + * Update values that haven't yet been dispatched because not all property animations have + * updated yet. + */ + private val undispatchedUpdates = + ArrayMap<FloatPropertyCompat<in T>, AnimationUpdate>() + + /** Called when a DynamicAnimation updates. */ + internal fun onInternalAnimationUpdate( + property: FloatPropertyCompat<in T>, + value: Float, + velocity: Float + ) { + + // If this property animation isn't relevant to this listener, ignore it. + if (!properties.contains(property)) { + return + } + + undispatchedUpdates[property] = AnimationUpdate(value, velocity) + maybeDispatchUpdates() + } + + /** + * Called when a DynamicAnimation ends. + * + * @return True if this listener should be removed from the list of internal listeners, so + * it no longer receives updates from DynamicAnimations. + */ + internal fun onInternalAnimationEnd( + property: FloatPropertyCompat<in T>, + canceled: Boolean, + finalValue: Float, + finalVelocity: Float, + isFling: Boolean + ): Boolean { + + // If this property animation isn't relevant to this listener, ignore it. + if (!properties.contains(property)) { + return false + } + + // Dispatch updates if we have one for each property. + numPropertiesAnimating-- + maybeDispatchUpdates() + + // If we didn't have an update for each property, dispatch the update for the ending + // property. This guarantees that an update isn't sent for this property *after* we call + // onAnimationEnd for that property. + if (undispatchedUpdates.contains(property)) { + updateListeners.forEach { updateListener -> + updateListener.onAnimationUpdateForProperty( + target, + UpdateMap<T>().also { it[property] = undispatchedUpdates[property] }) + } + + undispatchedUpdates.remove(property) + } + + val allEnded = !arePropertiesAnimating(properties) + endListeners.forEach { + it.onAnimationEnd( + target, property, isFling, canceled, finalValue, finalVelocity, + allEnded) + + // Check that the end listener didn't restart this property's animation. + if (isPropertyAnimating(property)) { + return false + } + } + + // If all of the animations that this listener cares about have ended, run the end + // actions unless the animation was canceled. + if (allEnded && !canceled) { + endActions.forEach { it() } + } + + return allEnded + } + + /** + * Dispatch undispatched values if we've received an update from each of the animating + * properties. + */ + private fun maybeDispatchUpdates() { + if (undispatchedUpdates.size >= numPropertiesAnimating && + undispatchedUpdates.size > 0) { + updateListeners.forEach { + it.onAnimationUpdateForProperty(target, ArrayMap(undispatchedUpdates)) + } + + undispatchedUpdates.clear() + } + } + } + + /** Return true if any animations are running on the object. */ + fun isRunning(): Boolean { + return arePropertiesAnimating(springAnimations.keys.union(flingAnimations.keys)) + } + + /** Returns whether the given property is animating. */ + fun isPropertyAnimating(property: FloatPropertyCompat<in T>): Boolean { + return springAnimations[property]?.isRunning ?: false || + flingAnimations[property]?.isRunning ?: false + } + + /** Returns whether any of the given properties are animating. */ + fun arePropertiesAnimating(properties: Set<FloatPropertyCompat<in T>>): Boolean { + return properties.any { isPropertyAnimating(it) } + } + + /** Return the set of properties that will begin animating upon calling [start]. */ + internal fun getAnimatedProperties(): Set<FloatPropertyCompat<in T>> { + return springConfigs.keys.union(flingConfigs.keys) + } + + /** + * Cancels the given properties. This is typically called immediately by [cancel], unless this + * animator is under test. + */ + internal fun cancelInternal(properties: Set<FloatPropertyCompat<in T>>) { + for (property in properties) { + flingAnimations[property]?.cancel() + springAnimations[property]?.cancel() + } + } + + /** Cancels all in progress animations on all properties. */ + fun cancel() { + cancelAction(flingAnimations.keys) + cancelAction(springAnimations.keys) + } + + /** Cancels in progress animations on the provided properties only. */ + fun cancel(vararg properties: FloatPropertyCompat<in T>) { + cancelAction(properties.toSet()) + } + + /** + * Container object for spring animation configuration settings. This allows you to store + * default stiffness and damping ratio values in a single configuration object, which you can + * pass to [spring]. + */ + data class SpringConfig internal constructor( + internal var stiffness: Float, + internal var dampingRatio: Float, + internal var startVelocity: Float = 0f, + internal var finalPosition: Float = UNSET + ) { + + constructor() : + this(globalDefaultSpring.stiffness, globalDefaultSpring.dampingRatio) + + constructor(stiffness: Float, dampingRatio: Float) : + this(stiffness = stiffness, dampingRatio = dampingRatio, startVelocity = 0f) + + /** Apply these configuration settings to the given SpringAnimation. */ + internal fun applyToAnimation(anim: SpringAnimation) { + val springForce = anim.spring ?: SpringForce() + anim.spring = springForce.apply { + stiffness = this@SpringConfig.stiffness + dampingRatio = this@SpringConfig.dampingRatio + finalPosition = this@SpringConfig.finalPosition + } + + if (startVelocity != 0f) anim.setStartVelocity(startVelocity) + } + } + + /** + * Container object for fling animation configuration settings. This allows you to store default + * friction values (as well as optional min/max values) in a single configuration object, which + * you can pass to [fling] and related methods. + */ + data class FlingConfig internal constructor( + internal var friction: Float, + internal var min: Float, + internal var max: Float, + internal var startVelocity: Float + ) { + + constructor() : this(globalDefaultFling.friction) + + constructor(friction: Float) : + this(friction, globalDefaultFling.min, globalDefaultFling.max) + + constructor(friction: Float, min: Float, max: Float) : + this(friction, min, max, startVelocity = 0f) + + /** Apply these configuration settings to the given FlingAnimation. */ + internal fun applyToAnimation(anim: FlingAnimation) { + anim.apply { + friction = this@FlingConfig.friction + setMinValue(min) + setMaxValue(max) + setStartVelocity(startVelocity) + } + } + } + + /** + * Listener for receiving values from in progress animations. Used with + * [PhysicsAnimator.addUpdateListener]. + * + * @param <T> The type of the object being animated. + </T> */ + interface UpdateListener<T> { + + /** + * Called on each animation frame with the target object, and a map of FloatPropertyCompat + * -> AnimationUpdate, containing the latest value and velocity for that property. When + * multiple properties are animating together, the map will typically contain one entry for + * each property. However, you should never assume that this is the case - when a property + * animation ends earlier than the others, you'll receive an UpdateMap containing only that + * property's final update. Subsequently, you'll only receive updates for the properties + * that are still animating. + * + * Always check that the map contains an update for the property you're interested in before + * accessing it. + * + * @param target The animated object itself. + * @param values Map of property to AnimationUpdate, which contains that property + * animation's latest value and velocity. You should never assume that a particular property + * is present in this map. + */ + fun onAnimationUpdateForProperty( + target: T, + values: UpdateMap<T> + ) + } + + /** + * Listener for receiving callbacks when animations end. + * + * @param <T> The type of the object being animated. + </T> */ + interface EndListener<T> { + + /** + * Called with the final animation values as each property animation ends. This can be used + * to respond to specific property animations concluding (such as hiding a view when ALPHA + * ends, even if the corresponding TRANSLATION animations have not ended). + * + * If you just want to run an action when all of the property animations have ended, you can + * use [PhysicsAnimator.withEndActions]. + * + * @param target The animated object itself. + * @param property The property whose animation has just ended. + * @param wasFling Whether this property ended after a fling animation (as opposed to a + * spring animation). If this property was animated via [flingThenSpring], this will be true + * if the fling animation did not reach the min/max bounds, decelerating to a stop + * naturally. It will be false if it hit the bounds and was sprung back. + * @param canceled Whether the animation was explicitly canceled before it naturally ended. + * @param finalValue The final value of the animated property. + * @param finalVelocity The final velocity (in pixels per second) of the ended animation. + * This is typically zero, unless this was a fling animation which ended abruptly due to + * reaching its configured min/max values. + * @param allRelevantPropertyAnimsEnded Whether all properties relevant to this end listener + * have ended. Relevant properties are those which were animated alongside the + * [addEndListener] call where this animator was passed in. For example: + * + * animator + * .spring(TRANSLATION_X, 100f) + * .spring(TRANSLATION_Y, 200f) + * .withEndListener(firstEndListener) + * .start() + * + * firstEndListener will be called first for TRANSLATION_X, with allEnded = false, + * because TRANSLATION_Y is still running. When TRANSLATION_Y ends, it'll be called with + * allEnded = true. + * + * If a subsequent call to start() is made with other properties, those properties are not + * considered relevant and allEnded will still equal true when only TRANSLATION_X and + * TRANSLATION_Y end. For example, if immediately after the prior example, while + * TRANSLATION_X and TRANSLATION_Y are still animating, we called: + * + * animator. + * .spring(SCALE_X, 2f, stiffness = 10f) // That will take awhile... + * .withEndListener(secondEndListener) + * .start() + * + * firstEndListener will still be called with allEnded = true when TRANSLATION_X/Y end, even + * though SCALE_X is still animating. Similarly, secondEndListener will be called with + * allEnded = true as soon as SCALE_X ends, even if the translation animations are still + * running. + */ + fun onAnimationEnd( + target: T, + property: FloatPropertyCompat<in T>, + wasFling: Boolean, + canceled: Boolean, + finalValue: Float, + finalVelocity: Float, + allRelevantPropertyAnimsEnded: Boolean + ) + } + + companion object { + + /** + * Constructor to use to for new physics animator instances in [getInstance]. This is + * typically the default constructor, but [PhysicsAnimatorTestUtils] can change it so that + * all code using the physics animator is given testable instances instead. + */ + internal var instanceConstructor: (Any) -> PhysicsAnimator<*> = ::PhysicsAnimator + + @JvmStatic + @Suppress("UNCHECKED_CAST") + fun <T : Any> getInstance(target: T): PhysicsAnimator<T> { + if (!animators.containsKey(target)) { + animators[target] = instanceConstructor(target) + } + + return animators[target] as PhysicsAnimator<T> + } + + /** + * Set whether all physics animators should log a lot of information about animations. + * Useful for debugging! + */ + @JvmStatic + fun setVerboseLogging(debug: Boolean) { + verboseLogging = debug + } + + /** + * Estimates the end value of a fling that starts at the given value using the provided + * start velocity and fling configuration. + * + * This is only an estimate. Fling animations use a timing-based physics simulation that is + * non-deterministic, so this exact value may not be reached. + */ + @JvmStatic + fun estimateFlingEndValue( + startValue: Float, + startVelocity: Float, + flingConfig: FlingConfig + ): Float { + val distance = startVelocity / (flingConfig.friction * FLING_FRICTION_SCALAR_MULTIPLIER) + return Math.min(flingConfig.max, Math.max(flingConfig.min, startValue + distance)) + } + + @JvmStatic + fun getReadablePropertyName(property: FloatPropertyCompat<*>): String { + return when (property) { + DynamicAnimation.TRANSLATION_X -> "translationX" + DynamicAnimation.TRANSLATION_Y -> "translationY" + DynamicAnimation.TRANSLATION_Z -> "translationZ" + DynamicAnimation.SCALE_X -> "scaleX" + DynamicAnimation.SCALE_Y -> "scaleY" + DynamicAnimation.ROTATION -> "rotation" + DynamicAnimation.ROTATION_X -> "rotationX" + DynamicAnimation.ROTATION_Y -> "rotationY" + DynamicAnimation.SCROLL_X -> "scrollX" + DynamicAnimation.SCROLL_Y -> "scrollY" + DynamicAnimation.ALPHA -> "alpha" + else -> "Custom FloatPropertyCompat instance" + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt new file mode 100644 index 000000000000..86eb8da952f1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt @@ -0,0 +1,485 @@ +/* + * 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.os.Handler +import android.os.Looper +import android.util.ArrayMap +import androidx.dynamicanimation.animation.FloatPropertyCompat +import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.prepareForTest +import java.util.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlin.collections.ArrayList +import kotlin.collections.HashMap +import kotlin.collections.HashSet +import kotlin.collections.Set +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.drop +import kotlin.collections.forEach +import kotlin.collections.getOrPut +import kotlin.collections.set +import kotlin.collections.toList +import kotlin.collections.toTypedArray + +typealias UpdateMatcher = (PhysicsAnimator.AnimationUpdate) -> Boolean +typealias UpdateFramesPerProperty<T> = + ArrayMap<FloatPropertyCompat<in T>, ArrayList<PhysicsAnimator.AnimationUpdate>> + +/** + * Utilities for testing code that uses [PhysicsAnimator]. + * + * Start by calling [prepareForTest] at the beginning of each test - this will modify the behavior + * of all PhysicsAnimator instances so that they post animations to the main thread (so they don't + * crash). It'll also enable the use of the other static helper methods in this class, which you can + * use to do things like block the test until animations complete (so you can test end states), or + * verify keyframes. + */ +object PhysicsAnimatorTestUtils { + var timeoutMs: Long = 2000 + private var startBlocksUntilAnimationsEnd = false + private val animationThreadHandler = Handler(Looper.getMainLooper()) + private val allAnimatedObjects = HashSet<Any>() + private val animatorTestHelpers = HashMap<PhysicsAnimator<*>, AnimatorTestHelper<*>>() + + /** + * Modifies the behavior of all [PhysicsAnimator] instances so that they post animations to the + * main thread, and report all of their + */ + @JvmStatic + fun prepareForTest() { + val defaultConstructor = PhysicsAnimator.instanceConstructor + PhysicsAnimator.instanceConstructor = fun(target: Any): PhysicsAnimator<*> { + val animator = defaultConstructor(target) + allAnimatedObjects.add(target) + animatorTestHelpers[animator] = AnimatorTestHelper(animator) + return animator + } + + timeoutMs = 2000 + startBlocksUntilAnimationsEnd = false + allAnimatedObjects.clear() + } + + @JvmStatic + fun tearDown() { + val latch = CountDownLatch(1) + animationThreadHandler.post { + animatorTestHelpers.keys.forEach { it.cancel() } + latch.countDown() + } + + latch.await() + + animatorTestHelpers.clear() + animators.clear() + allAnimatedObjects.clear() + } + + /** + * Sets the maximum time (in milliseconds) to block the test thread while waiting for animations + * before throwing an exception. + */ + @JvmStatic + fun setBlockTimeout(timeoutMs: Long) { + PhysicsAnimatorTestUtils.timeoutMs = timeoutMs + } + + /** + * Sets whether all animations should block the test thread until they end. This is typically + * the desired behavior, since you can invoke code that runs an animation and then assert things + * about its end state. + */ + @JvmStatic + fun setAllAnimationsBlock(block: Boolean) { + startBlocksUntilAnimationsEnd = block + } + + /** + * Blocks the calling thread until animations of the given property on the target object end. + */ + @JvmStatic + @Throws(InterruptedException::class) + fun <T : Any> blockUntilAnimationsEnd( + animator: PhysicsAnimator<T>, + vararg properties: FloatPropertyCompat<in T> + ) { + val animatingProperties = HashSet<FloatPropertyCompat<in T>>() + for (property in properties) { + if (animator.isPropertyAnimating(property)) { + animatingProperties.add(property) + } + } + + if (animatingProperties.size > 0) { + val latch = CountDownLatch(animatingProperties.size) + getAnimationTestHelper(animator).addTestEndListener( + object : PhysicsAnimator.EndListener<T> { + override fun onAnimationEnd( + target: T, + property: FloatPropertyCompat<in T>, + wasFling: Boolean, + canceled: Boolean, + finalValue: Float, + finalVelocity: Float, + allRelevantPropertyAnimsEnded: Boolean + ) { + if (animatingProperties.contains(property)) { + latch.countDown() + } + } + }) + + latch.await(timeoutMs, TimeUnit.MILLISECONDS) + } + } + + /** + * Blocks the calling thread until all animations of the given property (on all target objects) + * have ended. Useful when you don't have access to the objects being animated, but still need + * to wait for them to end so that other testable side effects occur (such as update/end + * listeners). + */ + @JvmStatic + @Throws(InterruptedException::class) + @Suppress("UNCHECKED_CAST") + fun <T : Any> blockUntilAnimationsEnd( + properties: FloatPropertyCompat<in T> + ) { + for (target in allAnimatedObjects) { + try { + blockUntilAnimationsEnd( + PhysicsAnimator.getInstance(target) as PhysicsAnimator<T>, properties) + } catch (e: ClassCastException) { + // Keep checking the other objects for ones whose types match the provided + // properties. + } + } + } + + /** + * Blocks the calling thread until the first animation frame in which predicate returns true. If + * the given object isn't animating, returns without blocking. + */ + @JvmStatic + @Throws(InterruptedException::class) + fun <T : Any> blockUntilFirstAnimationFrameWhereTrue( + animator: PhysicsAnimator<T>, + predicate: (T) -> Boolean + ) { + if (animator.isRunning()) { + val latch = CountDownLatch(1) + getAnimationTestHelper(animator).addTestUpdateListener(object : PhysicsAnimator + .UpdateListener<T> { + override fun onAnimationUpdateForProperty( + target: T, + values: UpdateMap<T> + ) { + if (predicate(target)) { + latch.countDown() + } + } + }) + + latch.await(timeoutMs, TimeUnit.MILLISECONDS) + } + } + + /** + * Verifies that the animator reported animation frame values to update listeners that satisfy + * the given matchers, in order. Not all frames need to satisfy a matcher - we'll run through + * all animation frames, and check them against the current predicate. If it returns false, we + * continue through the frames until it returns true, and then move on to the next matcher. + * Verification fails if we run out of frames while unsatisfied matchers remain. + * + * If verification is successful, all frames to this point are considered 'verified' and will be + * cleared. Subsequent calls to this method will start verification at the next animation frame. + * + * Example: Verify that an animation surpassed x = 50f before going negative. + * verifyAnimationUpdateFrames( + * animator, TRANSLATION_X, + * { u -> u.value > 50f }, + * { u -> u.value < 0f }) + * + * Example: verify that an animation went backwards at some point while still being on-screen. + * verifyAnimationUpdateFrames( + * animator, TRANSLATION_X, + * { u -> u.velocity < 0f && u.value >= 0f }) + * + * This method is intended to help you test longer, more complicated animations where it's + * critical that certain values were reached. Using this method to test short animations can + * fail due to the animation having fewer frames than provided matchers. For example, an + * animation from x = 1f to x = 5f might only have two frames, at x = 3f and x = 5f. The + * following would then fail despite it seeming logically sound: + * + * verifyAnimationUpdateFrames( + * animator, TRANSLATION_X, + * { u -> u.value > 1f }, + * { u -> u.value > 2f }, + * { u -> u.value > 3f }) + * + * Tests might also fail if your matchers are too granular, such as this example test after an + * animation from x = 0f to x = 100f. It's unlikely there was a frame specifically between 2f + * and 3f. + * + * verifyAnimationUpdateFrames( + * animator, TRANSLATION_X, + * { u -> u.value > 2f && u.value < 3f }, + * { u -> u.value >= 50f }) + * + * Failures will print a helpful log of all animation frames so you can see what caused the test + * to fail. + */ + fun <T : Any> verifyAnimationUpdateFrames( + animator: PhysicsAnimator<T>, + property: FloatPropertyCompat<in T>, + firstUpdateMatcher: UpdateMatcher, + vararg additionalUpdateMatchers: UpdateMatcher + ) { + val updateFrames: UpdateFramesPerProperty<T> = getAnimationUpdateFrames(animator) + + if (!updateFrames.containsKey(property)) { + error("No frames for given target object and property.") + } + + // Copy the frames to avoid a ConcurrentModificationException if the animation update + // listeners attempt to add a new frame while we're verifying these. + val framesForProperty = ArrayList(updateFrames[property]!!) + val matchers = ArrayDeque<UpdateMatcher>( + additionalUpdateMatchers.toList()) + val frameTraceMessage = StringBuilder() + + var curMatcher = firstUpdateMatcher + + // Loop through the updates from the testable animator. + for (update in framesForProperty) { + + // Check whether this frame satisfies the current matcher. + if (curMatcher(update)) { + + // If that was the last unsatisfied matcher, we're good here. 'Verify' all remaining + // frames and return without failing. + if (matchers.size == 0) { + getAnimationUpdateFrames(animator).remove(property) + return + } + + frameTraceMessage.append("$update\t(satisfied matcher)\n") + curMatcher = matchers.pop() // Get the next matcher and keep going. + } else { + frameTraceMessage.append("${update}\n") + } + } + + val readablePropertyName = PhysicsAnimator.getReadablePropertyName(property) + getAnimationUpdateFrames(animator).remove(property) + + throw RuntimeException( + "Failed to verify animation frames for property $readablePropertyName: " + + "Provided ${additionalUpdateMatchers.size + 1} matchers, " + + "however ${matchers.size + 1} remained unsatisfied.\n\n" + + "All frames:\n$frameTraceMessage") + } + + /** + * Overload of [verifyAnimationUpdateFrames] that builds matchers for you, from given float + * values. For example, to verify that an animations passed from 0f to 50f to 100f back to 50f: + * + * verifyAnimationUpdateFrames(animator, TRANSLATION_X, 0f, 50f, 100f, 50f) + * + * This verifies that update frames were received with values of >= 0f, >= 50f, >= 100f, and + * <= 50f. + * + * The same caveats apply: short animations might not have enough frames to satisfy all of the + * matchers, and overly specific calls (such as 0f, 1f, 2f, 3f, etc. for an animation from + * x = 0f to x = 100f) might fail as the animation only had frames at 0f, 25f, 50f, 75f, and + * 100f. As with [verifyAnimationUpdateFrames], failures will print a helpful log of all frames + * so you can see what caused the test to fail. + */ + fun <T : Any> verifyAnimationUpdateFrames( + animator: PhysicsAnimator<T>, + property: FloatPropertyCompat<in T>, + startValue: Float, + firstTargetValue: Float, + vararg additionalTargetValues: Float + ) { + val matchers = ArrayList<UpdateMatcher>() + + val values = ArrayList<Float>().also { + it.add(firstTargetValue) + it.addAll(additionalTargetValues.toList()) + } + + var prevVal = startValue + for (value in values) { + if (value > prevVal) { + matchers.add { update -> update.value >= value } + } else { + matchers.add { update -> update.value <= value } + } + + prevVal = value + } + + verifyAnimationUpdateFrames( + animator, property, matchers[0], *matchers.drop(0).toTypedArray()) + } + + /** + * Returns all of the values that have ever been reported to update listeners, per property. + */ + @Suppress("UNCHECKED_CAST") + fun <T : Any> getAnimationUpdateFrames(animator: PhysicsAnimator<T>): + UpdateFramesPerProperty<T> { + return animatorTestHelpers[animator]?.getUpdates() as UpdateFramesPerProperty<T> + } + + /** + * Clears animation frame updates from the given animator so they aren't used the next time its + * passed to [verifyAnimationUpdateFrames]. + */ + fun <T : Any> clearAnimationUpdateFrames(animator: PhysicsAnimator<T>) { + animatorTestHelpers[animator]?.clearUpdates() + } + + @Suppress("UNCHECKED_CAST") + private fun <T> getAnimationTestHelper(animator: PhysicsAnimator<T>): AnimatorTestHelper<T> { + return animatorTestHelpers[animator] as AnimatorTestHelper<T> + } + + /** + * Helper class for testing an animator. This replaces the animator's start action with + * [startForTest] and adds test listeners to enable other test utility behaviors. We build one + * these for each Animator and keep them around so we can access the updates. + */ + class AnimatorTestHelper<T> (private val animator: PhysicsAnimator<T>) { + + /** All updates received for each property animation. */ + private val allUpdates = + ArrayMap<FloatPropertyCompat<in T>, ArrayList<PhysicsAnimator.AnimationUpdate>>() + + private val testEndListeners = ArrayList<PhysicsAnimator.EndListener<T>>() + private val testUpdateListeners = ArrayList<PhysicsAnimator.UpdateListener<T>>() + + /** Whether we're currently in the middle of executing startInternal(). */ + private var currentlyRunningStartInternal = false + + init { + animator.startAction = ::startForTest + animator.cancelAction = ::cancelForTest + } + + internal fun addTestEndListener(listener: PhysicsAnimator.EndListener<T>) { + testEndListeners.add(listener) + } + + internal fun addTestUpdateListener(listener: PhysicsAnimator.UpdateListener<T>) { + testUpdateListeners.add(listener) + } + + internal fun getUpdates(): UpdateFramesPerProperty<T> { + return allUpdates + } + + internal fun clearUpdates() { + allUpdates.clear() + } + + private fun startForTest() { + // The testable animator needs to block the main thread until super.start() has been + // called, since callers expect .start() to be synchronous but we're posting it to a + // handler here. We may also continue blocking until all animations end, if + // startBlocksUntilAnimationsEnd = true. + val unblockLatch = CountDownLatch(if (startBlocksUntilAnimationsEnd) 2 else 1) + + animationThreadHandler.post { + // Add an update listener that dispatches to any test update listeners added by + // tests. + animator.addUpdateListener(object : PhysicsAnimator.UpdateListener<T> { + override fun onAnimationUpdateForProperty( + target: T, + values: ArrayMap<FloatPropertyCompat<in T>, PhysicsAnimator.AnimationUpdate> + ) { + values.forEach { (property, value) -> + allUpdates.getOrPut(property, { ArrayList() }).add(value) + } + + for (listener in testUpdateListeners) { + listener.onAnimationUpdateForProperty(target, values) + } + } + }) + + // Add an end listener that dispatches to any test end listeners added by tests, and + // unblocks the main thread if required. + animator.addEndListener(object : PhysicsAnimator.EndListener<T> { + override fun onAnimationEnd( + target: T, + property: FloatPropertyCompat<in T>, + wasFling: Boolean, + canceled: Boolean, + finalValue: Float, + finalVelocity: Float, + allRelevantPropertyAnimsEnded: Boolean + ) { + for (listener in testEndListeners) { + listener.onAnimationEnd( + target, property, wasFling, canceled, finalValue, finalVelocity, + allRelevantPropertyAnimsEnded) + } + + if (allRelevantPropertyAnimsEnded) { + testEndListeners.clear() + testUpdateListeners.clear() + + if (startBlocksUntilAnimationsEnd) { + unblockLatch.countDown() + } + } + } + }) + + currentlyRunningStartInternal = true + animator.startInternal() + currentlyRunningStartInternal = false + unblockLatch.countDown() + } + + unblockLatch.await(timeoutMs, TimeUnit.MILLISECONDS) + } + + private fun cancelForTest(properties: Set<FloatPropertyCompat<in T>>) { + // If this was called from startInternal, we are already on the animation thread, and + // should just call cancelInternal rather than posting it. If we post it, the + // cancellation will occur after the rest of startInternal() and we'll immediately + // cancel the animation we worked so hard to start! + if (currentlyRunningStartInternal) { + animator.cancelInternal(properties) + return + } + + val unblockLatch = CountDownLatch(1) + + animationThreadHandler.post { + animator.cancelInternal(properties) + unblockLatch.countDown() + } + + unblockLatch.await(timeoutMs, TimeUnit.MILLISECONDS) + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/AnimationThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/AnimationThread.java new file mode 100644 index 000000000000..96b9f86673fc --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/AnimationThread.java @@ -0,0 +1,61 @@ +/* + * 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 static android.os.Process.THREAD_PRIORITY_DISPLAY; + +import android.annotation.NonNull; +import android.os.HandlerThread; +import android.util.Singleton; + +/** + * A singleton thread for Shell to run animations on. + */ +public class AnimationThread extends HandlerThread { + private ShellExecutor mExecutor; + + private AnimationThread() { + super("wmshell.anim", THREAD_PRIORITY_DISPLAY); + } + + /** Get the singleton instance of this thread */ + public static AnimationThread instance() { + return sAnimationThreadSingleton.get(); + } + + /** + * @return a shared {@link ShellExecutor} associated with this thread + * @hide + */ + @NonNull + public ShellExecutor getExecutor() { + if (mExecutor == null) { + mExecutor = new HandlerExecutor(getThreadHandler()); + } + return mExecutor; + } + + private static final Singleton<AnimationThread> sAnimationThreadSingleton = + new Singleton<AnimationThread>() { + @Override + protected AnimationThread create() { + final AnimationThread animThread = new AnimationThread(); + animThread.start(); + return animThread; + } + }; +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java new file mode 100644 index 000000000000..976fba52b9e2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java @@ -0,0 +1,62 @@ +/* + * 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.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.view.Gravity; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import com.android.wm.shell.R; + +/** + * Circular view with a semitransparent, circular background with an 'X' inside it. + * + * This is used by both Bubbles and PIP as the dismiss target. + */ +public class DismissCircleView extends FrameLayout { + + private final ImageView mIconView = new ImageView(getContext()); + + public DismissCircleView(Context context) { + super(context); + final Resources res = getResources(); + + setBackground(res.getDrawable(R.drawable.dismiss_circle_background)); + + mIconView.setImageDrawable(res.getDrawable(R.drawable.pip_ic_close_white)); + addView(mIconView); + + setViewSizes(); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + setViewSizes(); + } + + /** Retrieves the current dimensions for the icon and circle and applies them. */ + private void setViewSizes() { + final Resources res = getResources(); + final int iconSize = res.getDimensionPixelSize(R.dimen.dismiss_target_x_size); + mIconView.setLayoutParams( + new FrameLayout.LayoutParams(iconSize, iconSize, Gravity.CENTER)); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java new file mode 100644 index 000000000000..3263f79888d6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2019 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.os.Handler; +import android.os.RemoteException; +import android.view.IDisplayWindowRotationCallback; +import android.view.IDisplayWindowRotationController; +import android.view.IWindowManager; +import android.window.WindowContainerTransaction; + +import java.util.ArrayList; + +/** + * This module deals with display rotations coming from WM. When WM starts a rotation: after it has + * frozen the screen, it will call into this class. This will then call all registered local + * controllers and give them a chance to queue up task changes to be applied synchronously with that + * rotation. + */ +public class DisplayChangeController { + + private final Handler mHandler; + private final IWindowManager mWmService; + + private final ArrayList<OnDisplayChangingListener> mRotationListener = + new ArrayList<>(); + private final ArrayList<OnDisplayChangingListener> mTmpListeners = new ArrayList<>(); + + private final IDisplayWindowRotationController mDisplayRotationController = + new IDisplayWindowRotationController.Stub() { + @Override + public void onRotateDisplay(int displayId, final int fromRotation, + final int toRotation, IDisplayWindowRotationCallback callback) { + mHandler.post(() -> { + WindowContainerTransaction t = new WindowContainerTransaction(); + synchronized (mRotationListener) { + mTmpListeners.clear(); + // Make a local copy in case the handlers add/remove themselves. + mTmpListeners.addAll(mRotationListener); + } + for (OnDisplayChangingListener c : mTmpListeners) { + c.onRotateDisplay(displayId, fromRotation, toRotation, t); + } + try { + callback.continueRotateDisplay(toRotation, t); + } catch (RemoteException e) { + } + }); + } + }; + + public DisplayChangeController(Handler mainHandler, IWindowManager wmService) { + mHandler = mainHandler; + mWmService = wmService; + try { + mWmService.setDisplayWindowRotationController(mDisplayRotationController); + } catch (RemoteException e) { + throw new RuntimeException("Unable to register rotation controller"); + } + } + + /** + * Adds a display rotation controller. + */ + public void addRotationListener(OnDisplayChangingListener listener) { + synchronized (mRotationListener) { + mRotationListener.add(listener); + } + } + + /** + * Removes a display rotation controller. + */ + public void removeRotationListener(OnDisplayChangingListener listener) { + synchronized (mRotationListener) { + mRotationListener.remove(listener); + } + } + + /** + * Give a listener a chance to queue up configuration changes to execute as part of a + * display rotation. The contents of {@link #onRotateDisplay} must run synchronously. + */ + public interface OnDisplayChangingListener { + /** + * Called before the display is rotated. Contents of this method must run synchronously. + * @param displayId Id of display that is rotating. + * @param fromRotation starting rotation of the display. + * @param toRotation target rotation of the display (after rotating). + * @param t A task transaction to populate. + */ + void onRotateDisplay(int displayId, int fromRotation, int toRotation, + WindowContainerTransaction t); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java new file mode 100644 index 000000000000..418973204add --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2019 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.Nullable; +import android.content.Context; +import android.content.res.Configuration; +import android.hardware.display.DisplayManager; +import android.os.Handler; +import android.os.RemoteException; +import android.util.Slog; +import android.util.SparseArray; +import android.view.Display; +import android.view.IDisplayWindowListener; +import android.view.IWindowManager; + +import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener; + +import java.util.ArrayList; + +/** + * This module deals with display rotations coming from WM. When WM starts a rotation: after it has + * frozen the screen, it will call into this class. This will then call all registered local + * controllers and give them a chance to queue up task changes to be applied synchronously with that + * rotation. + */ +public class DisplayController { + private static final String TAG = "DisplayController"; + + private final Handler mHandler; + private final Context mContext; + private final IWindowManager mWmService; + private final DisplayChangeController mChangeController; + + private final SparseArray<DisplayRecord> mDisplays = new SparseArray<>(); + private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>(); + + /** + * Gets a display by id from DisplayManager. + */ + public Display getDisplay(int displayId) { + final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); + return displayManager.getDisplay(displayId); + } + + private final IDisplayWindowListener mDisplayContainerListener = + new IDisplayWindowListener.Stub() { + @Override + public void onDisplayAdded(int displayId) { + mHandler.post(() -> { + synchronized (mDisplays) { + if (mDisplays.get(displayId) != null) { + return; + } + Display display = getDisplay(displayId); + if (display == null) { + // It's likely that the display is private to some app and thus not + // accessible by system-ui. + return; + } + DisplayRecord record = new DisplayRecord(); + record.mDisplayId = displayId; + record.mContext = (displayId == Display.DEFAULT_DISPLAY) ? mContext + : mContext.createDisplayContext(display); + record.mDisplayLayout = new DisplayLayout(record.mContext, display); + mDisplays.put(displayId, record); + for (int i = 0; i < mDisplayChangedListeners.size(); ++i) { + mDisplayChangedListeners.get(i).onDisplayAdded(displayId); + } + } + }); + } + + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + mHandler.post(() -> { + synchronized (mDisplays) { + DisplayRecord dr = mDisplays.get(displayId); + if (dr == null) { + Slog.w(TAG, "Skipping Display Configuration change on non-added" + + " display."); + return; + } + Display display = getDisplay(displayId); + if (display == null) { + Slog.w(TAG, "Skipping Display Configuration change on invalid" + + " display. It may have been removed."); + return; + } + Context perDisplayContext = mContext; + if (displayId != Display.DEFAULT_DISPLAY) { + perDisplayContext = mContext.createDisplayContext(display); + } + dr.mContext = perDisplayContext.createConfigurationContext(newConfig); + dr.mDisplayLayout = new DisplayLayout(dr.mContext, display); + for (int i = 0; i < mDisplayChangedListeners.size(); ++i) { + mDisplayChangedListeners.get(i).onDisplayConfigurationChanged( + displayId, newConfig); + } + } + }); + } + + @Override + public void onDisplayRemoved(int displayId) { + mHandler.post(() -> { + synchronized (mDisplays) { + if (mDisplays.get(displayId) == null) { + return; + } + for (int i = mDisplayChangedListeners.size() - 1; i >= 0; --i) { + mDisplayChangedListeners.get(i).onDisplayRemoved(displayId); + } + mDisplays.remove(displayId); + } + }); + } + + @Override + public void onFixedRotationStarted(int displayId, int newRotation) { + mHandler.post(() -> { + synchronized (mDisplays) { + if (mDisplays.get(displayId) == null || getDisplay(displayId) == null) { + Slog.w(TAG, "Skipping onFixedRotationStarted on unknown" + + " display, displayId=" + displayId); + return; + } + for (int i = mDisplayChangedListeners.size() - 1; i >= 0; --i) { + mDisplayChangedListeners.get(i).onFixedRotationStarted( + displayId, newRotation); + } + } + }); + } + + @Override + public void onFixedRotationFinished(int displayId) { + mHandler.post(() -> { + synchronized (mDisplays) { + if (mDisplays.get(displayId) == null || getDisplay(displayId) == null) { + Slog.w(TAG, "Skipping onFixedRotationFinished on unknown" + + " display, displayId=" + displayId); + return; + } + for (int i = mDisplayChangedListeners.size() - 1; i >= 0; --i) { + mDisplayChangedListeners.get(i).onFixedRotationFinished(displayId); + } + } + }); + } + }; + + public DisplayController(Context context, Handler handler, + IWindowManager wmService) { + mHandler = handler; + mContext = context; + mWmService = wmService; + mChangeController = new DisplayChangeController(mHandler, mWmService); + try { + mWmService.registerDisplayWindowListener(mDisplayContainerListener); + } catch (RemoteException e) { + throw new RuntimeException("Unable to register hierarchy listener"); + } + } + + /** + * Gets the DisplayLayout associated with a display. + */ + public @Nullable DisplayLayout getDisplayLayout(int displayId) { + final DisplayRecord r = mDisplays.get(displayId); + return r != null ? r.mDisplayLayout : null; + } + + /** + * Gets a display-specific context for a display. + */ + public @Nullable Context getDisplayContext(int displayId) { + final DisplayRecord r = mDisplays.get(displayId); + return r != null ? r.mContext : null; + } + + /** + * Add a display window-container listener. It will get notified whenever a display's + * configuration changes or when displays are added/removed from the WM hierarchy. + */ + public void addDisplayWindowListener(OnDisplaysChangedListener listener) { + synchronized (mDisplays) { + if (mDisplayChangedListeners.contains(listener)) { + return; + } + mDisplayChangedListeners.add(listener); + for (int i = 0; i < mDisplays.size(); ++i) { + listener.onDisplayAdded(mDisplays.keyAt(i)); + } + } + } + + /** + * Remove a display window-container listener. + */ + public void removeDisplayWindowListener(OnDisplaysChangedListener listener) { + synchronized (mDisplays) { + mDisplayChangedListeners.remove(listener); + } + } + + /** + * Adds a display rotation controller. + */ + public void addDisplayChangingController(OnDisplayChangingListener controller) { + mChangeController.addRotationListener(controller); + } + + /** + * Removes a display rotation controller. + */ + public void removeDisplayChangingController(OnDisplayChangingListener controller) { + mChangeController.removeRotationListener(controller); + } + + private static class DisplayRecord { + int mDisplayId; + Context mContext; + DisplayLayout mDisplayLayout; + } + + /** + * Gets notified when a display is added/removed to the WM hierarchy and when a display's + * window-configuration changes. + * + * @see IDisplayWindowListener + */ + public interface OnDisplaysChangedListener { + /** + * Called when a display has been added to the WM hierarchy. + */ + default void onDisplayAdded(int displayId) {} + + /** + * Called when a display's window-container configuration changes. + */ + default void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {} + + /** + * Called when a display is removed. + */ + default void onDisplayRemoved(int displayId) {} + + /** + * Called when fixed rotation on a display is started. + */ + default void onFixedRotationStarted(int displayId, int newRotation) {} + + /** + * Called when fixed rotation on a display is finished. + */ + default void onFixedRotationFinished(int displayId) {} + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java new file mode 100644 index 000000000000..ea18a19c2ee5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java @@ -0,0 +1,540 @@ +/* + * Copyright (C) 2019 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.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.IntDef; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Handler; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Slog; +import android.util.SparseArray; +import android.view.IDisplayWindowInsetsController; +import android.view.IWindowManager; +import android.view.InsetsSource; +import android.view.InsetsSourceControl; +import android.view.InsetsState; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.WindowInsets; +import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; + +import com.android.internal.view.IInputMethodManager; + +import java.util.ArrayList; +import java.util.concurrent.Executor; + +/** + * Manages IME control at the display-level. This occurs when IME comes up in multi-window mode. + */ +public class DisplayImeController implements DisplayController.OnDisplaysChangedListener { + private static final String TAG = "DisplayImeController"; + + private static final boolean DEBUG = false; + + // NOTE: All these constants came from InsetsController. + public static final int ANIMATION_DURATION_SHOW_MS = 275; + public static final int ANIMATION_DURATION_HIDE_MS = 340; + public static final Interpolator INTERPOLATOR = new PathInterpolator(0.4f, 0f, 0.2f, 1f); + private static final int DIRECTION_NONE = 0; + private static final int DIRECTION_SHOW = 1; + private static final int DIRECTION_HIDE = 2; + private static final int FLOATING_IME_BOTTOM_INSET = -80; + + protected final IWindowManager mWmService; + protected final Executor mExecutor; + private final TransactionPool mTransactionPool; + private final DisplayController mDisplayController; + private final SparseArray<PerDisplay> mImePerDisplay = new SparseArray<>(); + private final ArrayList<ImePositionProcessor> mPositionProcessors = new ArrayList<>(); + + + public DisplayImeController(IWindowManager wmService, DisplayController displayController, + Executor mainExecutor, TransactionPool transactionPool) { + mExecutor = mainExecutor; + mWmService = wmService; + mTransactionPool = transactionPool; + mDisplayController = displayController; + } + + /** Starts monitor displays changes and set insets controller for each displays. */ + public void startMonitorDisplays() { + mDisplayController.addDisplayWindowListener(this); + } + + @Override + public void onDisplayAdded(int displayId) { + // Add's a system-ui window-manager specifically for ime. This type is special because + // WM will defer IME inset handling to it in multi-window scenarious. + PerDisplay pd = new PerDisplay(displayId, + mDisplayController.getDisplayLayout(displayId).rotation()); + try { + mWmService.setDisplayWindowInsetsController(displayId, pd); + } catch (RemoteException e) { + Slog.w(TAG, "Unable to set insets controller on display " + displayId); + } + mImePerDisplay.put(displayId, pd); + } + + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + PerDisplay pd = mImePerDisplay.get(displayId); + if (pd == null) { + return; + } + if (mDisplayController.getDisplayLayout(displayId).rotation() + != pd.mRotation && isImeShowing(displayId)) { + pd.startAnimation(true, false /* forceRestart */); + } + } + + @Override + public void onDisplayRemoved(int displayId) { + try { + mWmService.setDisplayWindowInsetsController(displayId, null); + } catch (RemoteException e) { + Slog.w(TAG, "Unable to remove insets controller on display " + displayId); + } + mImePerDisplay.remove(displayId); + } + + private boolean isImeShowing(int displayId) { + PerDisplay pd = mImePerDisplay.get(displayId); + if (pd == null) { + return false; + } + final InsetsSource imeSource = pd.mInsetsState.getSource(InsetsState.ITYPE_IME); + return imeSource != null && pd.mImeSourceControl != null && imeSource.isVisible(); + } + + private void dispatchPositionChanged(int displayId, int imeTop, + SurfaceControl.Transaction t) { + synchronized (mPositionProcessors) { + for (ImePositionProcessor pp : mPositionProcessors) { + pp.onImePositionChanged(displayId, imeTop, t); + } + } + } + + @ImePositionProcessor.ImeAnimationFlags + private int dispatchStartPositioning(int displayId, int hiddenTop, int shownTop, + boolean show, boolean isFloating, SurfaceControl.Transaction t) { + synchronized (mPositionProcessors) { + int flags = 0; + for (ImePositionProcessor pp : mPositionProcessors) { + flags |= pp.onImeStartPositioning( + displayId, hiddenTop, shownTop, show, isFloating, t); + } + return flags; + } + } + + private void dispatchEndPositioning(int displayId, boolean cancel, + SurfaceControl.Transaction t) { + synchronized (mPositionProcessors) { + for (ImePositionProcessor pp : mPositionProcessors) { + pp.onImeEndPositioning(displayId, cancel, t); + } + } + } + + /** + * Adds an {@link ImePositionProcessor} to be called during ime position updates. + */ + public void addPositionProcessor(ImePositionProcessor processor) { + synchronized (mPositionProcessors) { + if (mPositionProcessors.contains(processor)) { + return; + } + mPositionProcessors.add(processor); + } + } + + /** + * Removes an {@link ImePositionProcessor} to be called during ime position updates. + */ + public void removePositionProcessor(ImePositionProcessor processor) { + synchronized (mPositionProcessors) { + mPositionProcessors.remove(processor); + } + } + + /** An implementation of {@link IDisplayWindowInsetsController} for a given display id. */ + public class PerDisplay extends IDisplayWindowInsetsController.Stub { + final int mDisplayId; + final InsetsState mInsetsState = new InsetsState(); + InsetsSourceControl mImeSourceControl = null; + int mAnimationDirection = DIRECTION_NONE; + ValueAnimator mAnimation = null; + int mRotation = Surface.ROTATION_0; + boolean mImeShowing = false; + final Rect mImeFrame = new Rect(); + boolean mAnimateAlpha = true; + + public PerDisplay(int displayId, int initialRotation) { + mDisplayId = displayId; + mRotation = initialRotation; + } + + @Override + public void insetsChanged(InsetsState insetsState) { + mExecutor.execute(() -> { + if (mInsetsState.equals(insetsState)) { + return; + } + + final InsetsSource newSource = insetsState.getSource(InsetsState.ITYPE_IME); + final Rect newFrame = newSource.getFrame(); + final Rect oldFrame = mInsetsState.getSource(InsetsState.ITYPE_IME).getFrame(); + + mInsetsState.set(insetsState, true /* copySources */); + if (mImeShowing && !newFrame.equals(oldFrame) && newSource.isVisible()) { + if (DEBUG) Slog.d(TAG, "insetsChanged when IME showing, restart animation"); + startAnimation(mImeShowing, true /* forceRestart */); + } + }); + } + + @Override + public void insetsControlChanged(InsetsState insetsState, + InsetsSourceControl[] activeControls) { + insetsChanged(insetsState); + if (activeControls != null) { + for (InsetsSourceControl activeControl : activeControls) { + if (activeControl == null) { + continue; + } + if (activeControl.getType() == InsetsState.ITYPE_IME) { + mExecutor.execute(() -> { + final Point lastSurfacePosition = mImeSourceControl != null + ? mImeSourceControl.getSurfacePosition() : null; + final boolean positionChanged = + !activeControl.getSurfacePosition().equals(lastSurfacePosition); + final boolean leashChanged = + !haveSameLeash(mImeSourceControl, activeControl); + mImeSourceControl = activeControl; + if (mAnimation != null) { + if (positionChanged) { + startAnimation(mImeShowing, true /* forceRestart */); + } + } else { + if (leashChanged) { + applyVisibilityToLeash(); + } + if (!mImeShowing) { + removeImeSurface(); + } + } + }); + } + } + } + } + + private void applyVisibilityToLeash() { + SurfaceControl leash = mImeSourceControl.getLeash(); + if (leash != null) { + SurfaceControl.Transaction t = mTransactionPool.acquire(); + if (mImeShowing) { + t.show(leash); + } else { + t.hide(leash); + } + t.apply(); + mTransactionPool.release(t); + } + } + + @Override + public void showInsets(int types, boolean fromIme) { + if ((types & WindowInsets.Type.ime()) == 0) { + return; + } + if (DEBUG) Slog.d(TAG, "Got showInsets for ime"); + mExecutor.execute(() -> startAnimation(true /* show */, false /* forceRestart */)); + } + + @Override + public void hideInsets(int types, boolean fromIme) { + if ((types & WindowInsets.Type.ime()) == 0) { + return; + } + if (DEBUG) Slog.d(TAG, "Got hideInsets for ime"); + mExecutor.execute(() -> startAnimation(false /* show */, false /* forceRestart */)); + } + + @Override + public void topFocusedWindowChanged(String packageName) { + // no-op + } + + /** + * Sends the local visibility state back to window manager. Needed for legacy adjustForIme. + */ + private void setVisibleDirectly(boolean visible) { + mInsetsState.getSource(InsetsState.ITYPE_IME).setVisible(visible); + try { + mWmService.modifyDisplayWindowInsets(mDisplayId, mInsetsState); + } catch (RemoteException e) { + } + } + + private int imeTop(float surfaceOffset) { + return mImeFrame.top + (int) surfaceOffset; + } + + private boolean calcIsFloating(InsetsSource imeSource) { + final Rect frame = imeSource.getFrame(); + if (frame.height() == 0) { + return true; + } + // Some Floating Input Methods will still report a frame, but the frame is actually + // a nav-bar inset created by WM and not part of the IME (despite being reported as + // an IME inset). For now, we assume that no non-floating IME will be <= this nav bar + // frame height so any reported frame that is <= nav-bar frame height is assumed to + // be floating. + return frame.height() <= mDisplayController.getDisplayLayout(mDisplayId) + .navBarFrameHeight(); + } + + private void startAnimation(final boolean show, final boolean forceRestart) { + final InsetsSource imeSource = mInsetsState.getSource(InsetsState.ITYPE_IME); + if (imeSource == null || mImeSourceControl == null) { + return; + } + final Rect newFrame = imeSource.getFrame(); + final boolean isFloating = calcIsFloating(imeSource) && show; + if (isFloating) { + // This is a "floating" or "expanded" IME, so to get animations, just + // pretend the ime has some size just below the screen. + mImeFrame.set(newFrame); + final int floatingInset = (int) (mDisplayController.getDisplayLayout(mDisplayId) + .density() * FLOATING_IME_BOTTOM_INSET); + mImeFrame.bottom -= floatingInset; + } else if (newFrame.height() != 0) { + // Don't set a new frame if it's empty and hiding -- this maintains continuity + mImeFrame.set(newFrame); + } + if (DEBUG) { + Slog.d(TAG, "Run startAnim show:" + show + " was:" + + (mAnimationDirection == DIRECTION_SHOW ? "SHOW" + : (mAnimationDirection == DIRECTION_HIDE ? "HIDE" : "NONE"))); + } + if (!forceRestart && (mAnimationDirection == DIRECTION_SHOW && show) + || (mAnimationDirection == DIRECTION_HIDE && !show)) { + return; + } + boolean seek = false; + float seekValue = 0; + if (mAnimation != null) { + if (mAnimation.isRunning()) { + seekValue = (float) mAnimation.getAnimatedValue(); + seek = true; + } + mAnimation.cancel(); + } + final float defaultY = mImeSourceControl.getSurfacePosition().y; + final float x = mImeSourceControl.getSurfacePosition().x; + final float hiddenY = defaultY + mImeFrame.height(); + final float shownY = defaultY; + final float startY = show ? hiddenY : shownY; + final float endY = show ? shownY : hiddenY; + if (mAnimationDirection == DIRECTION_NONE && mImeShowing && show) { + // IME is already showing, so set seek to end + seekValue = shownY; + seek = true; + } + mAnimationDirection = show ? DIRECTION_SHOW : DIRECTION_HIDE; + mImeShowing = show; + mAnimation = ValueAnimator.ofFloat(startY, endY); + mAnimation.setDuration( + show ? ANIMATION_DURATION_SHOW_MS : ANIMATION_DURATION_HIDE_MS); + if (seek) { + mAnimation.setCurrentFraction((seekValue - startY) / (endY - startY)); + } + + mAnimation.addUpdateListener(animation -> { + SurfaceControl.Transaction t = mTransactionPool.acquire(); + float value = (float) animation.getAnimatedValue(); + t.setPosition(mImeSourceControl.getLeash(), x, value); + final float alpha = (mAnimateAlpha || isFloating) + ? (value - hiddenY) / (shownY - hiddenY) : 1.f; + t.setAlpha(mImeSourceControl.getLeash(), alpha); + dispatchPositionChanged(mDisplayId, imeTop(value), t); + t.apply(); + mTransactionPool.release(t); + }); + mAnimation.setInterpolator(INTERPOLATOR); + mAnimation.addListener(new AnimatorListenerAdapter() { + private boolean mCancelled = false; + + @Override + public void onAnimationStart(Animator animation) { + SurfaceControl.Transaction t = mTransactionPool.acquire(); + t.setPosition(mImeSourceControl.getLeash(), x, startY); + if (DEBUG) { + Slog.d(TAG, "onAnimationStart d:" + mDisplayId + " top:" + + imeTop(hiddenY) + "->" + imeTop(shownY) + + " showing:" + (mAnimationDirection == DIRECTION_SHOW)); + } + int flags = dispatchStartPositioning(mDisplayId, imeTop(hiddenY), + imeTop(shownY), mAnimationDirection == DIRECTION_SHOW, isFloating, t); + mAnimateAlpha = (flags & ImePositionProcessor.IME_ANIMATION_NO_ALPHA) == 0; + final float alpha = (mAnimateAlpha || isFloating) + ? (startY - hiddenY) / (shownY - hiddenY) + : 1.f; + t.setAlpha(mImeSourceControl.getLeash(), alpha); + if (mAnimationDirection == DIRECTION_SHOW) { + t.show(mImeSourceControl.getLeash()); + } + t.apply(); + mTransactionPool.release(t); + } + + @Override + public void onAnimationCancel(Animator animation) { + mCancelled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (DEBUG) Slog.d(TAG, "onAnimationEnd " + mCancelled); + SurfaceControl.Transaction t = mTransactionPool.acquire(); + if (!mCancelled) { + t.setPosition(mImeSourceControl.getLeash(), x, endY); + t.setAlpha(mImeSourceControl.getLeash(), 1.f); + } + dispatchEndPositioning(mDisplayId, mCancelled, t); + if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) { + t.hide(mImeSourceControl.getLeash()); + removeImeSurface(); + } + t.apply(); + mTransactionPool.release(t); + + mAnimationDirection = DIRECTION_NONE; + mAnimation = null; + } + }); + if (!show) { + // When going away, queue up insets change first, otherwise any bounds changes + // can have a "flicker" of ime-provided insets. + setVisibleDirectly(false /* visible */); + } + mAnimation.start(); + if (show) { + // When showing away, queue up insets change last, otherwise any bounds changes + // can have a "flicker" of ime-provided insets. + setVisibleDirectly(true /* visible */); + } + } + } + + void removeImeSurface() { + final IInputMethodManager imms = getImms(); + if (imms != null) { + try { + // Remove the IME surface to make the insets invisible for + // non-client controlled insets. + imms.removeImeSurface(); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to remove IME surface.", e); + } + } + } + + /** + * Allows other things to synchronize with the ime position + */ + public interface ImePositionProcessor { + /** + * Indicates that ime shouldn't animate alpha. It will always be opaque. Used when stuff + * behind the IME shouldn't be visible (for example during split-screen adjustment where + * there is nothing behind the ime). + */ + int IME_ANIMATION_NO_ALPHA = 1; + + /** @hide */ + @IntDef(prefix = {"IME_ANIMATION_"}, value = { + IME_ANIMATION_NO_ALPHA, + }) + @interface ImeAnimationFlags { + } + + /** + * Called when the IME position is starting to animate. + * + * @param hiddenTop The y position of the top of the IME surface when it is hidden. + * @param shownTop The y position of the top of the IME surface when it is shown. + * @param showing {@code true} when we are animating from hidden to shown, {@code false} + * when animating from shown to hidden. + * @param isFloating {@code true} when the ime is a floating ime (doesn't inset). + * @return flags that may alter how ime itself is animated (eg. no-alpha). + */ + @ImeAnimationFlags + default int onImeStartPositioning(int displayId, int hiddenTop, int shownTop, + boolean showing, boolean isFloating, SurfaceControl.Transaction t) { + return 0; + } + + /** + * Called when the ime position changed. This is expected to be a synchronous call on the + * animation thread. Operations can be added to the transaction to be applied in sync. + * + * @param imeTop The current y position of the top of the IME surface. + */ + default void onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t) { + } + + /** + * Called when the IME position is done animating. + * + * @param cancel {@code true} if this was cancelled. This implies another start is coming. + */ + default void onImeEndPositioning(int displayId, boolean cancel, + SurfaceControl.Transaction t) { + } + } + + public IInputMethodManager getImms() { + return IInputMethodManager.Stub.asInterface( + ServiceManager.getService(Context.INPUT_METHOD_SERVICE)); + } + + private static boolean haveSameLeash(InsetsSourceControl a, InsetsSourceControl b) { + if (a == b) { + return true; + } + if (a == null || b == null) { + return false; + } + if (a.getLeash() == b.getLeash()) { + return true; + } + if (a.getLeash() == null || b.getLeash() == null) { + return false; + } + return a.getLeash().isSameSurface(b.getLeash()); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java new file mode 100644 index 000000000000..3181dbf74ace --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java @@ -0,0 +1,511 @@ +/* + * Copyright (C) 2019 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 static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.content.res.Configuration.UI_MODE_TYPE_CAR; +import static android.content.res.Configuration.UI_MODE_TYPE_MASK; +import static android.os.Process.SYSTEM_UID; +import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS; +import static android.view.Display.FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; +import static android.view.Surface.ROTATION_0; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Insets; +import android.graphics.Rect; +import android.os.SystemProperties; +import android.provider.Settings; +import android.util.DisplayMetrics; +import android.util.RotationUtils; +import android.util.Size; +import android.view.Display; +import android.view.DisplayCutout; +import android.view.DisplayInfo; +import android.view.Gravity; +import android.view.Surface; + +import com.android.internal.R; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Contains information about the layout-properties of a display. This refers to internal layout + * like insets/cutout/rotation. In general, this can be thought of as the shell analog to + * DisplayPolicy. + */ +public class DisplayLayout { + @IntDef(prefix = { "NAV_BAR_" }, value = { + NAV_BAR_LEFT, + NAV_BAR_RIGHT, + NAV_BAR_BOTTOM, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface NavBarPosition {} + + // Navigation bar position values + public static final int NAV_BAR_LEFT = 1 << 0; + public static final int NAV_BAR_RIGHT = 1 << 1; + public static final int NAV_BAR_BOTTOM = 1 << 2; + + private int mUiMode; + private int mWidth; + private int mHeight; + private DisplayCutout mCutout; + private int mRotation; + private int mDensityDpi; + private final Rect mNonDecorInsets = new Rect(); + private final Rect mStableInsets = new Rect(); + private boolean mHasNavigationBar = false; + private boolean mHasStatusBar = false; + private int mNavBarFrameHeight = 0; + + /** + * Create empty layout. + */ + public DisplayLayout() { + } + + /** + * Construct a custom display layout using a DisplayInfo. + * @param info + * @param res + */ + public DisplayLayout(DisplayInfo info, Resources res, boolean hasNavigationBar, + boolean hasStatusBar) { + init(info, res, hasNavigationBar, hasStatusBar); + } + + /** + * Construct a display layout based on a live display. + * @param context Used for resources. + */ + public DisplayLayout(@NonNull Context context, @NonNull Display rawDisplay) { + final int displayId = rawDisplay.getDisplayId(); + DisplayInfo info = new DisplayInfo(); + rawDisplay.getDisplayInfo(info); + init(info, context.getResources(), hasNavigationBar(info, context, displayId), + hasStatusBar(displayId)); + } + + public DisplayLayout(DisplayLayout dl) { + set(dl); + } + + /** sets this DisplayLayout to a copy of another on. */ + public void set(DisplayLayout dl) { + mUiMode = dl.mUiMode; + mWidth = dl.mWidth; + mHeight = dl.mHeight; + mCutout = dl.mCutout; + mRotation = dl.mRotation; + mDensityDpi = dl.mDensityDpi; + mHasNavigationBar = dl.mHasNavigationBar; + mHasStatusBar = dl.mHasStatusBar; + mNonDecorInsets.set(dl.mNonDecorInsets); + mStableInsets.set(dl.mStableInsets); + } + + private void init(DisplayInfo info, Resources res, boolean hasNavigationBar, + boolean hasStatusBar) { + mUiMode = res.getConfiguration().uiMode; + mWidth = info.logicalWidth; + mHeight = info.logicalHeight; + mRotation = info.rotation; + mCutout = info.displayCutout; + mDensityDpi = info.logicalDensityDpi; + mHasNavigationBar = hasNavigationBar; + mHasStatusBar = hasStatusBar; + recalcInsets(res); + } + + private void recalcInsets(Resources res) { + computeNonDecorInsets(res, mRotation, mWidth, mHeight, mCutout, mUiMode, mNonDecorInsets, + mHasNavigationBar); + mStableInsets.set(mNonDecorInsets); + if (mHasStatusBar) { + convertNonDecorInsetsToStableInsets(res, mStableInsets, mWidth, mHeight, mHasStatusBar); + } + mNavBarFrameHeight = getNavigationBarFrameHeight(res, mWidth > mHeight); + } + + /** + * Apply a rotation to this layout and its parameters. + * @param res + * @param targetRotation + */ + public void rotateTo(Resources res, @Surface.Rotation int targetRotation) { + final int rotationDelta = (targetRotation - mRotation + 4) % 4; + final boolean changeOrient = (rotationDelta % 2) != 0; + + final int origWidth = mWidth; + final int origHeight = mHeight; + + mRotation = targetRotation; + if (changeOrient) { + mWidth = origHeight; + mHeight = origWidth; + } + + if (mCutout != null && !mCutout.isEmpty()) { + mCutout = calculateDisplayCutoutForRotation(mCutout, rotationDelta, origWidth, + origHeight); + } + + recalcInsets(res); + } + + /** Get this layout's non-decor insets. */ + public Rect nonDecorInsets() { + return mNonDecorInsets; + } + + /** Get this layout's stable insets. */ + public Rect stableInsets() { + return mStableInsets; + } + + /** Get this layout's width. */ + public int width() { + return mWidth; + } + + /** Get this layout's height. */ + public int height() { + return mHeight; + } + + /** Get this layout's display rotation. */ + public int rotation() { + return mRotation; + } + + /** Get this layout's display density. */ + public int densityDpi() { + return mDensityDpi; + } + + /** Get the density scale for the display. */ + public float density() { + return mDensityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE; + } + + /** Get whether this layout is landscape. */ + public boolean isLandscape() { + return mWidth > mHeight; + } + + /** Get the navbar frame height (used by ime). */ + public int navBarFrameHeight() { + return mNavBarFrameHeight; + } + + /** Gets the orientation of this layout */ + public int getOrientation() { + return (mWidth > mHeight) ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT; + } + + /** Gets the calculated stable-bounds for this layout */ + public void getStableBounds(Rect outBounds) { + outBounds.set(0, 0, mWidth, mHeight); + outBounds.inset(mStableInsets); + } + + /** + * Gets navigation bar position for this layout + * @return Navigation bar position for this layout. + */ + public @NavBarPosition int getNavigationBarPosition(Resources res) { + return navigationBarPosition(res, mWidth, mHeight, mRotation); + } + + /** + * Rotates bounds as if parentBounds and bounds are a group. The group is rotated by `delta` + * 90-degree counter-clockwise increments. This assumes that parentBounds is at 0,0 and + * remains at 0,0 after rotation. + * + * Only 'bounds' is mutated. + */ + public static void rotateBounds(Rect inOutBounds, Rect parentBounds, int delta) { + int rdelta = ((delta % 4) + 4) % 4; + int origLeft = inOutBounds.left; + switch (rdelta) { + case 0: + return; + case 1: + inOutBounds.left = inOutBounds.top; + inOutBounds.top = parentBounds.right - inOutBounds.right; + inOutBounds.right = inOutBounds.bottom; + inOutBounds.bottom = parentBounds.right - origLeft; + return; + case 2: + inOutBounds.left = parentBounds.right - inOutBounds.right; + inOutBounds.right = parentBounds.right - origLeft; + return; + case 3: + inOutBounds.left = parentBounds.bottom - inOutBounds.bottom; + inOutBounds.bottom = inOutBounds.right; + inOutBounds.right = parentBounds.bottom - inOutBounds.top; + inOutBounds.top = origLeft; + return; + } + } + + /** + * Calculates the stable insets if we already have the non-decor insets. + */ + private static void convertNonDecorInsetsToStableInsets(Resources res, Rect inOutInsets, + int displayWidth, int displayHeight, boolean hasStatusBar) { + if (!hasStatusBar) { + return; + } + int statusBarHeight = getStatusBarHeight(displayWidth > displayHeight, res); + inOutInsets.top = Math.max(inOutInsets.top, statusBarHeight); + } + + /** + * Calculates the insets for the areas that could never be removed in Honeycomb, i.e. system + * bar or button bar. + * + * @param displayRotation the current display rotation + * @param displayWidth the current display width + * @param displayHeight the current display height + * @param displayCutout the current display cutout + * @param outInsets the insets to return + */ + static void computeNonDecorInsets(Resources res, int displayRotation, int displayWidth, + int displayHeight, DisplayCutout displayCutout, int uiMode, Rect outInsets, + boolean hasNavigationBar) { + outInsets.setEmpty(); + + // Only navigation bar + if (hasNavigationBar) { + int position = navigationBarPosition(res, displayWidth, displayHeight, displayRotation); + int navBarSize = + getNavigationBarSize(res, position, displayWidth > displayHeight, uiMode); + if (position == NAV_BAR_BOTTOM) { + outInsets.bottom = navBarSize; + } else if (position == NAV_BAR_RIGHT) { + outInsets.right = navBarSize; + } else if (position == NAV_BAR_LEFT) { + outInsets.left = navBarSize; + } + } + + if (displayCutout != null) { + outInsets.left += displayCutout.getSafeInsetLeft(); + outInsets.top += displayCutout.getSafeInsetTop(); + outInsets.right += displayCutout.getSafeInsetRight(); + outInsets.bottom += displayCutout.getSafeInsetBottom(); + } + } + + /** + * Calculates the stable insets without running a layout. + * + * @param displayRotation the current display rotation + * @param displayWidth the current display width + * @param displayHeight the current display height + * @param displayCutout the current display cutout + * @param outInsets the insets to return + */ + static void computeStableInsets(Resources res, int displayRotation, int displayWidth, + int displayHeight, DisplayCutout displayCutout, int uiMode, Rect outInsets, + boolean hasNavigationBar, boolean hasStatusBar) { + outInsets.setEmpty(); + + // Navigation bar and status bar. + computeNonDecorInsets(res, displayRotation, displayWidth, displayHeight, displayCutout, + uiMode, outInsets, hasNavigationBar); + convertNonDecorInsetsToStableInsets(res, outInsets, displayWidth, displayHeight, + hasStatusBar); + } + + /** Retrieve the statusbar height from resources. */ + static int getStatusBarHeight(boolean landscape, Resources res) { + return landscape ? res.getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_height_landscape) + : res.getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_height_portrait); + } + + /** Calculate the DisplayCutout for a particular display size/rotation. */ + public static DisplayCutout calculateDisplayCutoutForRotation( + DisplayCutout cutout, int rotation, int displayWidth, int displayHeight) { + if (cutout == null || cutout == DisplayCutout.NO_CUTOUT) { + return null; + } + final Insets waterfallInsets = + RotationUtils.rotateInsets(cutout.getWaterfallInsets(), rotation); + if (rotation == ROTATION_0) { + return computeSafeInsets(cutout, displayWidth, displayHeight); + } + final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270); + Rect[] cutoutRects = cutout.getBoundingRectsAll(); + final Rect[] newBounds = new Rect[cutoutRects.length]; + final Rect displayBounds = new Rect(0, 0, displayWidth, displayHeight); + for (int i = 0; i < cutoutRects.length; ++i) { + final Rect rect = new Rect(cutoutRects[i]); + if (!rect.isEmpty()) { + rotateBounds(rect, displayBounds, rotation); + } + newBounds[getBoundIndexFromRotation(i, rotation)] = rect; + } + return computeSafeInsets( + DisplayCutout.fromBoundsAndWaterfall(newBounds, waterfallInsets), + rotated ? displayHeight : displayWidth, + rotated ? displayWidth : displayHeight); + } + + private static int getBoundIndexFromRotation(int index, int rotation) { + return (index - rotation) < 0 + ? index - rotation + DisplayCutout.BOUNDS_POSITION_LENGTH + : index - rotation; + } + + /** Calculate safe insets. */ + public static DisplayCutout computeSafeInsets(DisplayCutout inner, + int displayWidth, int displayHeight) { + if (inner == DisplayCutout.NO_CUTOUT) { + return null; + } + + final Size displaySize = new Size(displayWidth, displayHeight); + final Rect safeInsets = computeSafeInsets(displaySize, inner); + return inner.replaceSafeInsets(safeInsets); + } + + private static Rect computeSafeInsets( + Size displaySize, DisplayCutout cutout) { + if (displaySize.getWidth() == displaySize.getHeight()) { + throw new UnsupportedOperationException("not implemented: display=" + displaySize + + " cutout=" + cutout); + } + + int leftInset = Math.max(cutout.getWaterfallInsets().left, + findCutoutInsetForSide(displaySize, cutout.getBoundingRectLeft(), Gravity.LEFT)); + int topInset = Math.max(cutout.getWaterfallInsets().top, + findCutoutInsetForSide(displaySize, cutout.getBoundingRectTop(), Gravity.TOP)); + int rightInset = Math.max(cutout.getWaterfallInsets().right, + findCutoutInsetForSide(displaySize, cutout.getBoundingRectRight(), Gravity.RIGHT)); + int bottomInset = Math.max(cutout.getWaterfallInsets().bottom, + findCutoutInsetForSide(displaySize, cutout.getBoundingRectBottom(), + Gravity.BOTTOM)); + + return new Rect(leftInset, topInset, rightInset, bottomInset); + } + + private static int findCutoutInsetForSide(Size display, Rect boundingRect, int gravity) { + if (boundingRect.isEmpty()) { + return 0; + } + + int inset = 0; + switch (gravity) { + case Gravity.TOP: + return Math.max(inset, boundingRect.bottom); + case Gravity.BOTTOM: + return Math.max(inset, display.getHeight() - boundingRect.top); + case Gravity.LEFT: + return Math.max(inset, boundingRect.right); + case Gravity.RIGHT: + return Math.max(inset, display.getWidth() - boundingRect.left); + default: + throw new IllegalArgumentException("unknown gravity: " + gravity); + } + } + + static boolean hasNavigationBar(DisplayInfo info, Context context, int displayId) { + if (displayId == Display.DEFAULT_DISPLAY) { + // Allow a system property to override this. Used by the emulator. + final String navBarOverride = SystemProperties.get("qemu.hw.mainkeys"); + if ("1".equals(navBarOverride)) { + return false; + } else if ("0".equals(navBarOverride)) { + return true; + } + return context.getResources().getBoolean(R.bool.config_showNavigationBar); + } else { + boolean isUntrustedVirtualDisplay = info.type == Display.TYPE_VIRTUAL + && info.ownerUid != SYSTEM_UID; + final ContentResolver resolver = context.getContentResolver(); + boolean forceDesktopOnExternal = Settings.Global.getInt(resolver, + DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 0) != 0; + + return ((info.flags & FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS) != 0 + || (forceDesktopOnExternal && !isUntrustedVirtualDisplay)); + // TODO(b/142569966): make sure VR2D and DisplayWindowSettings are moved here somehow. + } + } + + static boolean hasStatusBar(int displayId) { + return displayId == Display.DEFAULT_DISPLAY; + } + + /** Retrieve navigation bar position from resources based on rotation and size. */ + public static @NavBarPosition int navigationBarPosition(Resources res, int displayWidth, + int displayHeight, int rotation) { + boolean navBarCanMove = displayWidth != displayHeight && res.getBoolean( + com.android.internal.R.bool.config_navBarCanMove); + if (navBarCanMove && displayWidth > displayHeight) { + if (rotation == Surface.ROTATION_90) { + return NAV_BAR_RIGHT; + } else { + return NAV_BAR_LEFT; + } + } + return NAV_BAR_BOTTOM; + } + + /** Retrieve navigation bar size from resources based on side/orientation/ui-mode */ + public static int getNavigationBarSize(Resources res, int navBarSide, boolean landscape, + int uiMode) { + final boolean carMode = (uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_CAR; + if (carMode) { + if (navBarSide == NAV_BAR_BOTTOM) { + return res.getDimensionPixelSize(landscape + ? R.dimen.navigation_bar_height_landscape_car_mode + : R.dimen.navigation_bar_height_car_mode); + } else { + return res.getDimensionPixelSize(R.dimen.navigation_bar_width_car_mode); + } + + } else { + if (navBarSide == NAV_BAR_BOTTOM) { + return res.getDimensionPixelSize(landscape + ? R.dimen.navigation_bar_height_landscape + : R.dimen.navigation_bar_height); + } else { + return res.getDimensionPixelSize(R.dimen.navigation_bar_width); + } + } + } + + /** @see com.android.server.wm.DisplayPolicy#getNavigationBarFrameHeight */ + public static int getNavigationBarFrameHeight(Resources res, boolean landscape) { + return res.getDimensionPixelSize(landscape + ? R.dimen.navigation_bar_frame_height_landscape + : R.dimen.navigation_bar_frame_height); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/FloatingContentCoordinator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/FloatingContentCoordinator.kt new file mode 100644 index 000000000000..d5d072a8d449 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/FloatingContentCoordinator.kt @@ -0,0 +1,368 @@ +/* + * 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.graphics.Rect +import android.util.Log +import com.android.wm.shell.common.FloatingContentCoordinator.FloatingContent +import java.util.HashMap + +/** Tag for debug logging. */ +private const val TAG = "FloatingCoordinator" + +/** + * Coordinates the positions and movement of floating content, such as PIP and Bubbles, to ensure + * that they don't overlap. If content does overlap due to content appearing or moving, the + * coordinator will ask content to move to resolve the conflict. + * + * After implementing [FloatingContent], content should call [onContentAdded] to begin coordination. + * Subsequently, call [onContentMoved] whenever the content moves, and the coordinator will move + * other content out of the way. [onContentRemoved] should be called when the content is removed or + * no longer visible. + */ + +class FloatingContentCoordinator constructor() { + /** + * Represents a piece of floating content, such as PIP or the Bubbles stack. Provides methods + * that allow the [FloatingContentCoordinator] to determine the current location of the content, + * as well as the ability to ask it to move out of the way of other content. + * + * The default implementation of [calculateNewBoundsOnOverlap] moves the content up or down, + * depending on the position of the conflicting content. You can override this method if you + * want your own custom conflict resolution logic. + */ + interface FloatingContent { + + /** + * Return the bounds claimed by this content. This should include the bounds occupied by the + * content itself, as well as any padding, if desired. The coordinator will ensure that no + * other content is located within these bounds. + * + * If the content is animating, this method should return the bounds to which the content is + * animating. If that animation is cancelled, or updated, be sure that your implementation + * of this method returns the appropriate bounds, and call [onContentMoved] so that the + * coordinator moves other content out of the way. + */ + fun getFloatingBoundsOnScreen(): Rect + + /** + * Return the area within which this floating content is allowed to move. When resolving + * conflicts, the coordinator will never ask your content to move to a position where any + * part of the content would be out of these bounds. + */ + fun getAllowedFloatingBoundsRegion(): Rect + + /** + * Called when the coordinator needs this content to move to the given bounds. It's up to + * you how to do that. + * + * Note that if you start an animation to these bounds, [getFloatingBoundsOnScreen] should + * return the destination bounds, not the in-progress animated bounds. This is so the + * coordinator knows where floating content is going to be and can resolve conflicts + * accordingly. + */ + fun moveToBounds(bounds: Rect) + + /** + * Called by the coordinator when it needs to find a new home for this floating content, + * because a new or moving piece of content is now overlapping with it. + * + * [findAreaForContentVertically] and [findAreaForContentAboveOrBelow] are helpful utility + * functions that will find new bounds for your content automatically. Unless you require + * specific conflict resolution logic, these should be sufficient. By default, this method + * delegates to [findAreaForContentVertically]. + * + * @param overlappingContentBounds The bounds of the other piece of content, which + * necessitated this content's relocation. Your new position must not overlap with these + * bounds. + * @param otherContentBounds The bounds of any other pieces of floating content. Your new + * position must not overlap with any of these either. These bounds are guaranteed to be + * non-overlapping. + * @return The new bounds for this content. + */ + @JvmDefault + fun calculateNewBoundsOnOverlap( + overlappingContentBounds: Rect, + otherContentBounds: List<Rect> + ): Rect { + return findAreaForContentVertically( + getFloatingBoundsOnScreen(), + overlappingContentBounds, + otherContentBounds, + getAllowedFloatingBoundsRegion()) + } + } + + /** The bounds of all pieces of floating content added to the coordinator. */ + private val allContentBounds: MutableMap<FloatingContent, Rect> = HashMap() + + /** + * Whether we are currently resolving conflicts by asking content to move. If we are, we'll + * temporarily ignore calls to [onContentMoved] - those calls are from the content that is + * moving to new, conflict-free bounds, so we don't need to perform conflict detection + * calculations in response. + */ + private var currentlyResolvingConflicts = false + + /** + * Makes the coordinator aware of a new piece of floating content, and moves any existing + * content out of the way, if necessary. + * + * If you don't want your new content to move existing content, use [getOccupiedBounds] to find + * an unoccupied area, and move the content there before calling this method. + */ + fun onContentAdded(newContent: FloatingContent) { + updateContentBounds() + allContentBounds[newContent] = newContent.getFloatingBoundsOnScreen() + maybeMoveConflictingContent(newContent) + } + + /** + * Called to notify the coordinator that a piece of floating content has moved (or is animating) + * to a new position, and that any conflicting floating content should be moved out of the way. + * + * The coordinator will call [FloatingContent.getFloatingBoundsOnScreen] to find the new bounds + * for the moving content. If you're animating the content, be sure that your implementation of + * getFloatingBoundsOnScreen returns the bounds to which it's animating, not the content's + * current bounds. + * + * If the animation moving this content is cancelled or updated, you'll need to call this method + * again, to ensure that content is moved out of the way of the latest bounds. + * + * @param content The content that has moved. + */ + fun onContentMoved(content: FloatingContent) { + + // Ignore calls when we are currently resolving conflicts, since those calls are from + // content that is moving to new, conflict-free bounds. + if (currentlyResolvingConflicts) { + return + } + + if (!allContentBounds.containsKey(content)) { + Log.wtf(TAG, "Received onContentMoved call before onContentAdded! " + + "This should never happen.") + return + } + + updateContentBounds() + maybeMoveConflictingContent(content) + } + + /** + * Called to notify the coordinator that a piece of floating content has been removed or is no + * longer visible. + */ + fun onContentRemoved(removedContent: FloatingContent) { + allContentBounds.remove(removedContent) + } + + /** + * Returns a set of Rects that represent the bounds of all of the floating content on the + * screen. + * + * [onContentAdded] will move existing content out of the way if the added content intersects + * existing content. That's fine - but if your specific starting position is not important, you + * can use this function to find unoccupied space for your content before calling + * [onContentAdded], so that moving existing content isn't necessary. + */ + fun getOccupiedBounds(): Collection<Rect> { + return allContentBounds.values + } + + /** + * Identifies any pieces of content that are now overlapping with the given content, and asks + * them to move out of the way. + */ + private fun maybeMoveConflictingContent(fromContent: FloatingContent) { + currentlyResolvingConflicts = true + + val conflictingNewBounds = allContentBounds[fromContent]!! + allContentBounds + // Filter to content that intersects with the new bounds. That's content that needs + // to move. + .filter { (content, bounds) -> + content != fromContent && Rect.intersects(conflictingNewBounds, bounds) } + // Tell that content to get out of the way, and save the bounds it says it's moving + // (or animating) to. + .forEach { (content, bounds) -> + val newBounds = content.calculateNewBoundsOnOverlap( + conflictingNewBounds, + // Pass all of the content bounds except the bounds of the + // content we're asking to move, and the conflicting new bounds + // (since those are passed separately). + otherContentBounds = allContentBounds.values + .minus(bounds) + .minus(conflictingNewBounds)) + + // If the new bounds are empty, it means there's no non-overlapping position + // that is in bounds. Just leave the content where it is. This should normally + // not happen, but sometimes content like PIP reports incorrect bounds + // temporarily. + if (!newBounds.isEmpty) { + content.moveToBounds(newBounds) + allContentBounds[content] = content.getFloatingBoundsOnScreen() + } + } + + currentlyResolvingConflicts = false + } + + /** + * Update [allContentBounds] by calling [FloatingContent.getFloatingBoundsOnScreen] for all + * content and saving the result. + */ + private fun updateContentBounds() { + allContentBounds.keys.forEach { allContentBounds[it] = it.getFloatingBoundsOnScreen() } + } + + companion object { + /** + * Finds new bounds for the given content, either above or below its current position. The + * new bounds won't intersect with the newly overlapping rect or the exclusion rects, and + * will be within the allowed bounds unless no possible position exists. + * + * You can use this method to help find a new position for your content when the coordinator + * calls [FloatingContent.moveToAreaExcluding]. + * + * @param contentRect The bounds of the content for which we're finding a new home. + * @param newlyOverlappingRect The bounds of the content that forced this relocation by + * intersecting with the content we now need to move. If the overlapping content is + * overlapping the top half of this content, we'll try to move this content downward if + * possible (since the other content is 'pushing' it down), and vice versa. + * @param exclusionRects Any other areas that we need to avoid when finding a new home for + * the content. These areas must be non-overlapping with each other. + * @param allowedBounds The area within which we're allowed to find new bounds for the + * content. + * @return New bounds for the content that don't intersect the exclusion rects or the + * newly overlapping rect, and that is within bounds - or an empty Rect if no in-bounds + * position exists. + */ + @JvmStatic + fun findAreaForContentVertically( + contentRect: Rect, + newlyOverlappingRect: Rect, + exclusionRects: Collection<Rect>, + allowedBounds: Rect + ): Rect { + // If the newly overlapping Rect's center is above the content's center, we'll prefer to + // find a space for this content that is below the overlapping content, since it's + // 'pushing' it down. This may not be possible due to to screen bounds, in which case + // we'll find space in the other direction. + val overlappingContentPushingDown = + newlyOverlappingRect.centerY() < contentRect.centerY() + + // Filter to exclusion rects that are above or below the content that we're finding a + // place for. Then, split into two lists - rects above the content, and rects below it. + var (rectsToAvoidAbove, rectsToAvoidBelow) = exclusionRects + .filter { rectToAvoid -> rectsIntersectVertically(rectToAvoid, contentRect) } + .partition { rectToAvoid -> rectToAvoid.top < contentRect.top } + + // Lazily calculate the closest possible new tops for the content, above and below its + // current location. + val newContentBoundsAbove by lazy { + findAreaForContentAboveOrBelow( + contentRect, + exclusionRects = rectsToAvoidAbove.plus(newlyOverlappingRect), + findAbove = true) + } + val newContentBoundsBelow by lazy { + findAreaForContentAboveOrBelow( + contentRect, + exclusionRects = rectsToAvoidBelow.plus(newlyOverlappingRect), + findAbove = false) + } + + val positionAboveInBounds by lazy { allowedBounds.contains(newContentBoundsAbove) } + val positionBelowInBounds by lazy { allowedBounds.contains(newContentBoundsBelow) } + + // Use the 'below' position if the content is being overlapped from the top, unless it's + // out of bounds. Also use it if the content is being overlapped from the bottom, but + // the 'above' position is out of bounds. Otherwise, use the 'above' position. + val usePositionBelow = + overlappingContentPushingDown && positionBelowInBounds || + !overlappingContentPushingDown && !positionAboveInBounds + + // Return the content rect, but offset to reflect the new position. + val newBounds = if (usePositionBelow) newContentBoundsBelow else newContentBoundsAbove + + // If the new bounds are within the allowed bounds, return them. If not, it means that + // there are no legal new bounds. This can happen if the new content's bounds are too + // large (for example, full-screen PIP). Since there is no reasonable action to take + // here, return an empty Rect and we will just not move the content. + return if (allowedBounds.contains(newBounds)) newBounds else Rect() + } + + /** + * Finds a new position for the given content, either above or below its current position + * depending on whether [findAbove] is true or false, respectively. This new position will + * not intersect with any of the [exclusionRects]. + * + * This method is useful as a helper method for implementing your own conflict resolution + * logic. Otherwise, you'd want to use [findAreaForContentVertically], which takes screen + * bounds and conflicting bounds' location into account when deciding whether to move to new + * bounds above or below the current bounds. + * + * @param contentRect The content we're finding an area for. + * @param exclusionRects The areas we need to avoid when finding a new area for the content. + * These areas must be non-overlapping with each other. + * @param findAbove Whether we are finding an area above the content's current position, + * rather than an area below it. + */ + fun findAreaForContentAboveOrBelow( + contentRect: Rect, + exclusionRects: Collection<Rect>, + findAbove: Boolean + ): Rect { + // Sort the rects, since we want to move the content as little as possible. We'll + // start with the rects closest to the content and move outward. If we're finding an + // area above the content, that means we sort in reverse order to search the rects + // from highest to lowest y-value. + val sortedExclusionRects = + exclusionRects.sortedBy { if (findAbove) -it.top else it.top } + + val proposedNewBounds = Rect(contentRect) + for (exclusionRect in sortedExclusionRects) { + // If the proposed new bounds don't intersect with this exclusion rect, that + // means there's room for the content here. We know this because the rects are + // sorted and non-overlapping, so any subsequent exclusion rects would be higher + // (or lower) than this one and can't possibly intersect if this one doesn't. + if (!Rect.intersects(proposedNewBounds, exclusionRect)) { + break + } else { + // Otherwise, we need to keep searching for new bounds. If we're finding an + // area above, propose new bounds that place the content just above the + // exclusion rect. If we're finding an area below, propose new bounds that + // place the content just below the exclusion rect. + val verticalOffset = + if (findAbove) -contentRect.height() else exclusionRect.height() + proposedNewBounds.offsetTo( + proposedNewBounds.left, + exclusionRect.top + verticalOffset) + } + } + + return proposedNewBounds + } + + /** Returns whether or not the two Rects share any of the same space on the X axis. */ + private fun rectsIntersectVertically(r1: Rect, r2: Rect): Boolean { + return (r1.left >= r2.left && r1.left <= r2.right) || + (r1.right <= r2.right && r1.right >= r2.left) + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java new file mode 100644 index 000000000000..cd75840b8c71 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java @@ -0,0 +1,48 @@ +/* + * 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; + +/** Executor implementation which is backed by a Handler. */ +public class HandlerExecutor implements ShellExecutor { + private final Handler mHandler; + + public HandlerExecutor(@NonNull Handler handler) { + mHandler = handler; + } + + @Override + public void executeDelayed(@NonNull Runnable r, long delayMillis) { + if (!mHandler.postDelayed(r, delayMillis)) { + throw new RuntimeException(mHandler + " is probably exiting"); + } + } + + @Override + public void removeCallbacks(@NonNull Runnable r) { + mHandler.removeCallbacks(r); + } + + @Override + public void execute(@NonNull Runnable command) { + if (!mHandler.post(command)) { + throw new RuntimeException(mHandler + " is probably exiting"); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java new file mode 100644 index 000000000000..aafe2407a1ea --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java @@ -0,0 +1,35 @@ +/* + * 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 java.util.concurrent.Executor; + +/** + * Super basic Executor interface that adds support for delayed execution and removing callbacks. + * Intended to wrap Handler while better-supporting testing. + */ +public interface ShellExecutor extends Executor { + /** + * See {@link android.os.Handler#postDelayed(Runnable, long)}. + */ + void executeDelayed(Runnable r, long delayMillis); + + /** + * See {@link android.os.Handler#removeCallbacks}. + */ + void removeCallbacks(Runnable r); +} 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/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java new file mode 100644 index 000000000000..24381d937e2f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java @@ -0,0 +1,365 @@ +/* + * Copyright (C) 2019 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 static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; + +import android.annotation.NonNull; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Point; +import android.graphics.Region; +import android.os.Bundle; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.util.MergedConfiguration; +import android.util.Slog; +import android.util.SparseArray; +import android.view.Display; +import android.view.DragEvent; +import android.view.IScrollCaptureCallbacks; +import android.view.IWindow; +import android.view.IWindowManager; +import android.view.IWindowSession; +import android.view.IWindowSessionCallback; +import android.view.InsetsSourceControl; +import android.view.InsetsState; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.WindowlessWindowManager; +import android.window.ClientWindowFrames; + +import com.android.internal.os.IResultReceiver; + +import java.util.HashMap; + +/** + * Represents the "windowing" layer of the WM Shell. This layer allows shell components to place and + * manipulate windows without talking to WindowManager. + */ +public class SystemWindows { + private static final String TAG = "SystemWindows"; + + private final SparseArray<PerDisplay> mPerDisplay = new SparseArray<>(); + private final HashMap<View, SurfaceControlViewHost> mViewRoots = new HashMap<>(); + private final DisplayController mDisplayController; + private final IWindowManager mWmService; + private IWindowSession mSession; + + private final DisplayController.OnDisplaysChangedListener mDisplayListener = + new DisplayController.OnDisplaysChangedListener() { + @Override + public void onDisplayAdded(int displayId) { } + + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + PerDisplay pd = mPerDisplay.get(displayId); + if (pd == null) { + return; + } + pd.updateConfiguration(newConfig); + } + + @Override + public void onDisplayRemoved(int displayId) { } + }; + + public SystemWindows(DisplayController displayController, IWindowManager wmService) { + mWmService = wmService; + mDisplayController = displayController; + mDisplayController.addDisplayWindowListener(mDisplayListener); + try { + mSession = wmService.openSession( + new IWindowSessionCallback.Stub() { + @Override + public void onAnimatorScaleChanged(float scale) {} + }); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to create layer", e); + } + } + + /** + * Adds a view to system-ui window management. + */ + public void addView(View view, WindowManager.LayoutParams attrs, int displayId, + int windowType) { + PerDisplay pd = mPerDisplay.get(displayId); + if (pd == null) { + pd = new PerDisplay(displayId); + mPerDisplay.put(displayId, pd); + } + pd.addView(view, attrs, windowType); + } + + /** + * Removes a view from system-ui window management. + * @param view + */ + public void removeView(View view) { + SurfaceControlViewHost root = mViewRoots.remove(view); + root.release(); + } + + /** + * Updates the layout params of a view. + */ + public void updateViewLayout(@NonNull View view, ViewGroup.LayoutParams params) { + SurfaceControlViewHost root = mViewRoots.get(view); + if (root == null || !(params instanceof WindowManager.LayoutParams)) { + return; + } + view.setLayoutParams(params); + root.relayout((WindowManager.LayoutParams) params); + } + + /** + * Sets the touchable region of a view's window. This will be cropped to the window size. + * @param view + * @param region + */ + public void setTouchableRegion(@NonNull View view, Region region) { + SurfaceControlViewHost root = mViewRoots.get(view); + if (root == null) { + return; + } + WindowlessWindowManager wwm = root.getWindowlessWM(); + if (!(wwm instanceof SysUiWindowManager)) { + return; + } + ((SysUiWindowManager) wwm).setTouchableRegionForWindow(view, region); + } + + /** + * Adds a root for system-ui window management with no views. Only useful for IME. + */ + public void addRoot(int displayId, int windowType) { + PerDisplay pd = mPerDisplay.get(displayId); + if (pd == null) { + pd = new PerDisplay(displayId); + mPerDisplay.put(displayId, pd); + } + pd.addRoot(windowType); + } + + /** + * Get the IWindow token for a specific root. + * + * @param windowType A window type from {@link WindowManager}. + */ + IWindow getWindow(int displayId, int windowType) { + PerDisplay pd = mPerDisplay.get(displayId); + if (pd == null) { + return null; + } + return pd.getWindow(windowType); + } + + /** + * Gets the SurfaceControl associated with a root view. This is the same surface that backs the + * ViewRootImpl. + */ + public SurfaceControl getViewSurface(View rootView) { + for (int i = 0; i < mPerDisplay.size(); ++i) { + for (int iWm = 0; iWm < mPerDisplay.valueAt(i).mWwms.size(); ++iWm) { + SurfaceControl out = mPerDisplay.valueAt(i).mWwms.valueAt(iWm) + .getSurfaceControlForWindow(rootView); + if (out != null) { + return out; + } + } + } + return null; + } + + private class PerDisplay { + final int mDisplayId; + private final SparseArray<SysUiWindowManager> mWwms = new SparseArray<>(); + + PerDisplay(int displayId) { + mDisplayId = displayId; + } + + public void addView(View view, WindowManager.LayoutParams attrs, int windowType) { + SysUiWindowManager wwm = addRoot(windowType); + if (wwm == null) { + Slog.e(TAG, "Unable to create systemui root"); + return; + } + final Display display = mDisplayController.getDisplay(mDisplayId); + SurfaceControlViewHost viewRoot = + new SurfaceControlViewHost( + view.getContext(), display, wwm, true /* useSfChoreographer */); + attrs.flags |= FLAG_HARDWARE_ACCELERATED; + viewRoot.setView(view, attrs); + mViewRoots.put(view, viewRoot); + + try { + mWmService.setShellRootAccessibilityWindow(mDisplayId, windowType, + viewRoot.getWindowToken()); + } catch (RemoteException e) { + Slog.e(TAG, "Error setting accessibility window for " + mDisplayId + ":" + + windowType, e); + } + } + + SysUiWindowManager addRoot(int windowType) { + SysUiWindowManager wwm = mWwms.get(windowType); + if (wwm != null) { + return wwm; + } + SurfaceControl rootSurface = null; + ContainerWindow win = new ContainerWindow(); + try { + rootSurface = mWmService.addShellRoot(mDisplayId, win, windowType); + } catch (RemoteException e) { + } + if (rootSurface == null) { + Slog.e(TAG, "Unable to get root surfacecontrol for systemui"); + return null; + } + Context displayContext = mDisplayController.getDisplayContext(mDisplayId); + wwm = new SysUiWindowManager(mDisplayId, displayContext, rootSurface, win); + mWwms.put(windowType, wwm); + return wwm; + } + + IWindow getWindow(int windowType) { + SysUiWindowManager wwm = mWwms.get(windowType); + if (wwm == null) { + return null; + } + return wwm.mContainerWindow; + } + + void updateConfiguration(Configuration configuration) { + for (int i = 0; i < mWwms.size(); ++i) { + mWwms.valueAt(i).updateConfiguration(configuration); + } + } + } + + /** + * A subclass of WindowlessWindowManager that provides insets to its viewroots. + */ + public class SysUiWindowManager extends WindowlessWindowManager { + final int mDisplayId; + ContainerWindow mContainerWindow; + public SysUiWindowManager(int displayId, Context ctx, SurfaceControl rootSurface, + ContainerWindow container) { + super(ctx.getResources().getConfiguration(), rootSurface, null /* hostInputToken */); + mContainerWindow = container; + mDisplayId = displayId; + } + + void updateConfiguration(Configuration configuration) { + setConfiguration(configuration); + } + + SurfaceControl getSurfaceControlForWindow(View rootView) { + return getSurfaceControl(rootView); + } + + void setTouchableRegionForWindow(View rootView, Region region) { + IBinder token = rootView.getWindowToken(); + if (token == null) { + return; + } + setTouchRegion(token, region); + } + } + + static class ContainerWindow extends IWindow.Stub { + ContainerWindow() {} + + @Override + public void resized(ClientWindowFrames frames, boolean reportDraw, + MergedConfiguration newMergedConfiguration, boolean forceLayout, + boolean alwaysConsumeSystemBars, int displayId) {} + + @Override + public void locationInParentDisplayChanged(Point offset) {} + + @Override + public void insetsChanged(InsetsState insetsState) {} + + @Override + public void insetsControlChanged(InsetsState insetsState, + InsetsSourceControl[] activeControls) {} + + @Override + public void showInsets(int types, boolean fromIme) {} + + @Override + public void hideInsets(int types, boolean fromIme) {} + + @Override + public void moved(int newX, int newY) {} + + @Override + public void dispatchAppVisibility(boolean visible) {} + + @Override + public void dispatchGetNewSurface() {} + + @Override + public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) {} + + @Override + public void executeCommand(String command, String parameters, ParcelFileDescriptor out) {} + + @Override + public void closeSystemDialogs(String reason) {} + + @Override + public void dispatchWallpaperOffsets(float x, float y, float xStep, float yStep, + float zoom, boolean sync) {} + + @Override + public void dispatchWallpaperCommand(String action, int x, int y, + int z, Bundle extras, boolean sync) {} + + /* Drag/drop */ + @Override + public void dispatchDragEvent(DragEvent event) {} + + @Override + public void updatePointerIcon(float x, float y) {} + + @Override + public void dispatchWindowShown() {} + + @Override + public void requestAppKeyboardShortcuts(IResultReceiver receiver, int deviceId) {} + + @Override + public void dispatchPointerCaptureChanged(boolean hasCapture) {} + + @Override + public void requestScrollCapture(IScrollCaptureCallbacks callbacks) { + try { + callbacks.onUnavailable(); + } catch (RemoteException ex) { + // ignore + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TransactionPool.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TransactionPool.java new file mode 100644 index 000000000000..4c34566b0d98 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TransactionPool.java @@ -0,0 +1,50 @@ +/* + * 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.util.Pools; +import android.view.SurfaceControl; + +/** + * Provides a synchronized pool of {@link SurfaceControl.Transaction}s to minimize allocations. + */ +public class TransactionPool { + private final Pools.SynchronizedPool<SurfaceControl.Transaction> mTransactionPool = + new Pools.SynchronizedPool<>(4); + + public TransactionPool() { + } + + /** Gets a transaction from the pool. */ + public SurfaceControl.Transaction acquire() { + SurfaceControl.Transaction t = mTransactionPool.acquire(); + if (t == null) { + return new SurfaceControl.Transaction(); + } + return t; + } + + /** + * Return a transaction to the pool. DO NOT call {@link SurfaceControl.Transaction#close()} if + * returning to pool. + */ + public void release(SurfaceControl.Transaction t) { + if (!mTransactionPool.release(t)) { + t.close(); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt new file mode 100644 index 000000000000..b4d738712893 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt @@ -0,0 +1,699 @@ +/* + * 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.magnetictarget + +import android.annotation.SuppressLint +import android.content.Context +import android.database.ContentObserver +import android.graphics.PointF +import android.os.Handler +import android.os.UserHandle +import android.os.VibrationEffect +import android.os.Vibrator +import android.provider.Settings +import android.view.MotionEvent +import android.view.VelocityTracker +import android.view.View +import android.view.ViewConfiguration +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.FloatPropertyCompat +import androidx.dynamicanimation.animation.SpringForce +import com.android.wm.shell.animation.PhysicsAnimator +import kotlin.math.abs +import kotlin.math.hypot + +/** + * Utility class for creating 'magnetized' objects that are attracted to one or more magnetic + * targets. Magnetic targets attract objects that are dragged near them, and hold them there unless + * they're moved away or released. Releasing objects inside a magnetic target typically performs an + * action on the object. + * + * MagnetizedObject also supports flinging to targets, which will result in the object being pulled + * into the target and released as if it was dragged into it. + * + * To use this class, either construct an instance with an object of arbitrary type, or use the + * [MagnetizedObject.magnetizeView] shortcut method if you're magnetizing a view. Then, set + * [magnetListener] to receive event callbacks. In your touch handler, pass all MotionEvents + * that move this object to [maybeConsumeMotionEvent]. If that method returns true, consider the + * event consumed by the MagnetizedObject and don't move the object unless it begins returning false + * again. + * + * @param context Context, used to retrieve a Vibrator instance for vibration effects. + * @param underlyingObject The actual object that we're magnetizing. + * @param xProperty Property that sets the x value of the object's position. + * @param yProperty Property that sets the y value of the object's position. + */ +abstract class MagnetizedObject<T : Any>( + val context: Context, + + /** The actual object that is animated. */ + val underlyingObject: T, + + /** Property that gets/sets the object's X value. */ + val xProperty: FloatPropertyCompat<in T>, + + /** Property that gets/sets the object's Y value. */ + val yProperty: FloatPropertyCompat<in T> +) { + + /** Return the width of the object. */ + abstract fun getWidth(underlyingObject: T): Float + + /** Return the height of the object. */ + abstract fun getHeight(underlyingObject: T): Float + + /** + * Fill the provided array with the location of the top-left of the object, relative to the + * entire screen. Compare to [View.getLocationOnScreen]. + */ + abstract fun getLocationOnScreen(underlyingObject: T, loc: IntArray) + + /** Methods for listening to events involving a magnetized object. */ + interface MagnetListener { + + /** + * Called when touch events move within the magnetic field of a target, causing the + * object to animate to the target and become 'stuck' there. The animation happens + * automatically here - you should not move the object. You can, however, change its state + * to indicate to the user that it's inside the target and releasing it will have an effect. + * + * [maybeConsumeMotionEvent] is now returning true and will continue to do so until a call + * to [onUnstuckFromTarget] or [onReleasedInTarget]. + * + * @param target The target that the object is now stuck to. + */ + fun onStuckToTarget(target: MagneticTarget) + + /** + * Called when the object is no longer stuck to a target. This means that either touch + * events moved outside of the magnetic field radius, or that a forceful fling out of the + * target was detected. + * + * The object won't be automatically animated out of the target, since you're responsible + * for moving the object again. You should move it (or animate it) using your own + * movement/animation logic. + * + * Reverse any effects applied in [onStuckToTarget] here. + * + * If [wasFlungOut] is true, [maybeConsumeMotionEvent] returned true for the ACTION_UP event + * that concluded the fling. If [wasFlungOut] is false, that means a drag gesture is ongoing + * and [maybeConsumeMotionEvent] is now returning false. + * + * @param target The target that this object was just unstuck from. + * @param velX The X velocity of the touch gesture when it exited the magnetic field. + * @param velY The Y velocity of the touch gesture when it exited the magnetic field. + * @param wasFlungOut Whether the object was unstuck via a fling gesture. This means that + * an ACTION_UP event was received, and that the gesture velocity was sufficient to conclude + * that the user wants to un-stick the object despite no touch events occurring outside of + * the magnetic field radius. + */ + fun onUnstuckFromTarget( + target: MagneticTarget, + velX: Float, + velY: Float, + wasFlungOut: Boolean + ) + + /** + * Called when the object is released inside a target, or flung towards it with enough + * velocity to reach it. + * + * @param target The target that the object was released in. + */ + fun onReleasedInTarget(target: MagneticTarget) + } + + private val animator: PhysicsAnimator<T> = PhysicsAnimator.getInstance(underlyingObject) + private val objectLocationOnScreen = IntArray(2) + + /** + * Targets that have been added to this object. These will all be considered when determining + * magnetic fields and fling trajectories. + */ + private val associatedTargets = ArrayList<MagneticTarget>() + + private val velocityTracker: VelocityTracker = VelocityTracker.obtain() + private val vibrator: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + + private var touchDown = PointF() + private var touchSlop = 0 + private var movedBeyondSlop = false + + /** Whether touch events are presently occurring within the magnetic field area of a target. */ + val objectStuckToTarget: Boolean + get() = targetObjectIsStuckTo != null + + /** The target the object is stuck to, or null if the object is not stuck to any target. */ + private var targetObjectIsStuckTo: MagneticTarget? = null + + /** + * Sets the listener to receive events. This must be set, or [maybeConsumeMotionEvent] + * will always return false and no magnetic effects will occur. + */ + lateinit var magnetListener: MagnetizedObject.MagnetListener + + /** + * Optional update listener to provide to the PhysicsAnimator that is used to spring the object + * into the target. + */ + var physicsAnimatorUpdateListener: PhysicsAnimator.UpdateListener<T>? = null + + /** + * Optional end listener to provide to the PhysicsAnimator that is used to spring the object + * into the target. + */ + var physicsAnimatorEndListener: PhysicsAnimator.EndListener<T>? = null + + /** + * Method that is called when the object should be animated stuck to the target. The default + * implementation uses the object's x and y properties to animate the object centered inside the + * target. You can override this if you need custom animation. + * + * The method is invoked with the MagneticTarget that the object is sticking to, the X and Y + * velocities of the gesture that brought the object into the magnetic radius, whether or not it + * was flung, and a callback you must call after your animation completes. + */ + var animateStuckToTarget: (MagneticTarget, Float, Float, Boolean, (() -> Unit)?) -> Unit = + ::animateStuckToTargetInternal + + /** + * Sets whether forcefully flinging the object vertically towards a target causes it to be + * attracted to the target and then released immediately, despite never being dragged within the + * magnetic field. + */ + var flingToTargetEnabled = true + + /** + * If fling to target is enabled, forcefully flinging the object towards a target will cause + * it to be attracted to the target and then released immediately, despite never being dragged + * within the magnetic field. + * + * This sets the width of the area considered 'near' enough a target to be considered a fling, + * in terms of percent of the target view's width. For example, setting this to 3f means that + * flings towards a 100px-wide target will be considered 'near' enough if they're towards the + * 300px-wide area around the target. + * + * Flings whose trajectory intersects the area will be attracted and released - even if the + * target view itself isn't intersected: + * + * | | + * | 0 | + * | / | + * | / | + * | X / | + * |.....###.....| + * + * + * Flings towards the target whose trajectories do not intersect the area will be treated as + * normal flings and the magnet will leave the object alone: + * + * | | + * | | + * | 0 | + * | / | + * | / X | + * |.....###.....| + * + */ + var flingToTargetWidthPercent = 3f + + /** + * Sets the minimum velocity (in pixels per second) required to fling an object to the target + * without dragging it into the magnetic field. + */ + var flingToTargetMinVelocity = 4000f + + /** + * Sets the minimum velocity (in pixels per second) required to fling un-stuck an object stuck + * to the target. If this velocity is reached, the object will be freed even if it wasn't moved + * outside the magnetic field radius. + */ + var flingUnstuckFromTargetMinVelocity = 4000f + + /** + * Sets the maximum X velocity above which the object will not stick to the target. Even if the + * object is dragged through the magnetic field, it will not stick to the target until the + * horizontal velocity is below this value. + */ + var stickToTargetMaxXVelocity = 2000f + + /** + * Enable or disable haptic vibration effects when the object interacts with the magnetic field. + * + * If you're experiencing crashes when the object enters targets, ensure that you have the + * android.permission.VIBRATE permission! + */ + var hapticsEnabled = true + + /** Default spring configuration to use for animating the object into a target. */ + var springConfig = PhysicsAnimator.SpringConfig( + SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_NO_BOUNCY) + + /** + * Spring configuration to use to spring the object into a target specifically when it's flung + * towards (rather than dragged near) it. + */ + var flungIntoTargetSpringConfig = springConfig + + init { + initHapticSettingObserver(context) + } + + /** + * Adds the provided MagneticTarget to this object. The object will now be attracted to the + * target if it strays within its magnetic field or is flung towards it. + * + * If this target (or its magnetic field) overlaps another target added to this object, the + * prior target will take priority. + */ + fun addTarget(target: MagneticTarget) { + associatedTargets.add(target) + target.updateLocationOnScreen() + } + + /** + * Shortcut that accepts a View and a magnetic field radius and adds it as a magnetic target. + * + * @return The MagneticTarget instance for the given View. This can be used to change the + * target's magnetic field radius after it's been added. It can also be added to other + * magnetized objects. + */ + fun addTarget(target: View, magneticFieldRadiusPx: Int): MagneticTarget { + return MagneticTarget(target, magneticFieldRadiusPx).also { addTarget(it) } + } + + /** + * Removes the given target from this object. The target will no longer attract the object. + */ + fun removeTarget(target: MagneticTarget) { + associatedTargets.remove(target) + } + + /** + * Provide this method with all motion events that move the magnetized object. If the + * location of the motion events moves within the magnetic field of a target, or indicate a + * fling-to-target gesture, this method will return true and you should not move the object + * yourself until it returns false again. + * + * Note that even when this method returns true, you should continue to pass along new motion + * events so that we know when the events move back outside the magnetic field area. + * + * This method will always return false if you haven't set a [magnetListener]. + */ + fun maybeConsumeMotionEvent(ev: MotionEvent): Boolean { + // Short-circuit if we don't have a listener or any targets, since those are required. + if (associatedTargets.size == 0) { + return false + } + + // When a gesture begins, recalculate target views' positions on the screen in case they + // have changed. Also, clear state. + if (ev.action == MotionEvent.ACTION_DOWN) { + updateTargetViews() + + // Clear the velocity tracker and stuck target. + velocityTracker.clear() + targetObjectIsStuckTo = null + + // Set the touch down coordinates and reset movedBeyondSlop. + touchDown.set(ev.rawX, ev.rawY) + movedBeyondSlop = false + } + + // Always pass events to the VelocityTracker. + addMovement(ev) + + // If we haven't yet moved beyond the slop distance, check if we have. + if (!movedBeyondSlop) { + val dragDistance = hypot(ev.rawX - touchDown.x, ev.rawY - touchDown.y) + if (dragDistance > touchSlop) { + // If we're beyond the slop distance, save that and continue. + movedBeyondSlop = true + } else { + // Otherwise, don't do anything yet. + return false + } + } + + val targetObjectIsInMagneticFieldOf = associatedTargets.firstOrNull { target -> + val distanceFromTargetCenter = hypot( + ev.rawX - target.centerOnScreen.x, + ev.rawY - target.centerOnScreen.y) + distanceFromTargetCenter < target.magneticFieldRadiusPx + } + + // If we aren't currently stuck to a target, and we're in the magnetic field of a target, + // we're newly stuck. + val objectNewlyStuckToTarget = + !objectStuckToTarget && targetObjectIsInMagneticFieldOf != null + + // If we are currently stuck to a target, we're in the magnetic field of a target, and that + // target isn't the one we're currently stuck to, then touch events have moved into a + // adjacent target's magnetic field. + val objectMovedIntoDifferentTarget = + objectStuckToTarget && + targetObjectIsInMagneticFieldOf != null && + targetObjectIsStuckTo != targetObjectIsInMagneticFieldOf + + if (objectNewlyStuckToTarget || objectMovedIntoDifferentTarget) { + velocityTracker.computeCurrentVelocity(1000) + val velX = velocityTracker.xVelocity + val velY = velocityTracker.yVelocity + + // If the object is moving too quickly within the magnetic field, do not stick it. This + // only applies to objects newly stuck to a target. If the object is moved into a new + // target, it wasn't moving at all (since it was stuck to the previous one). + if (objectNewlyStuckToTarget && abs(velX) > stickToTargetMaxXVelocity) { + return false + } + + // This touch event is newly within the magnetic field - let the listener know, and + // animate sticking to the magnet. + targetObjectIsStuckTo = targetObjectIsInMagneticFieldOf + cancelAnimations() + magnetListener.onStuckToTarget(targetObjectIsInMagneticFieldOf!!) + animateStuckToTarget(targetObjectIsInMagneticFieldOf, velX, velY, false, null) + + vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK) + } else if (targetObjectIsInMagneticFieldOf == null && objectStuckToTarget) { + velocityTracker.computeCurrentVelocity(1000) + + // This touch event is newly outside the magnetic field - let the listener know. It will + // move the object out of the target using its own movement logic. + cancelAnimations() + magnetListener.onUnstuckFromTarget( + targetObjectIsStuckTo!!, velocityTracker.xVelocity, velocityTracker.yVelocity, + wasFlungOut = false) + targetObjectIsStuckTo = null + + vibrateIfEnabled(VibrationEffect.EFFECT_TICK) + } + + // First, check for relevant gestures concluding with an ACTION_UP. + if (ev.action == MotionEvent.ACTION_UP) { + + velocityTracker.computeCurrentVelocity(1000 /* units */) + val velX = velocityTracker.xVelocity + val velY = velocityTracker.yVelocity + + // Cancel the magnetic animation since we might still be springing into the magnetic + // target, but we're about to fling away or release. + cancelAnimations() + + if (objectStuckToTarget) { + if (-velY > flingUnstuckFromTargetMinVelocity) { + // If the object is stuck, but it was forcefully flung away from the target in + // the upward direction, tell the listener so the object can be animated out of + // the target. + magnetListener.onUnstuckFromTarget( + targetObjectIsStuckTo!!, velX, velY, wasFlungOut = true) + } else { + // If the object is stuck and not flung away, it was released inside the target. + magnetListener.onReleasedInTarget(targetObjectIsStuckTo!!) + vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK) + } + + // Either way, we're no longer stuck. + targetObjectIsStuckTo = null + return true + } + + // The target we're flinging towards, or null if we're not flinging towards any target. + val flungToTarget = associatedTargets.firstOrNull { target -> + isForcefulFlingTowardsTarget(target, ev.rawX, ev.rawY, velX, velY) + } + + if (flungToTarget != null) { + // If this is a fling-to-target, animate the object to the magnet and then release + // it. + magnetListener.onStuckToTarget(flungToTarget) + targetObjectIsStuckTo = flungToTarget + + animateStuckToTarget(flungToTarget, velX, velY, true) { + magnetListener.onReleasedInTarget(flungToTarget) + targetObjectIsStuckTo = null + vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK) + } + + return true + } + + // If it's not either of those things, we are not interested. + return false + } + + return objectStuckToTarget // Always consume touch events if the object is stuck. + } + + /** Plays the given vibration effect if haptics are enabled. */ + @SuppressLint("MissingPermission") + private fun vibrateIfEnabled(effect: Int) { + if (hapticsEnabled && systemHapticsEnabled) { + vibrator.vibrate(effect.toLong()) + } + } + + /** Adds the movement to the velocity tracker using raw coordinates. */ + private fun addMovement(event: MotionEvent) { + // Add movement to velocity tracker using raw screen X and Y coordinates instead + // of window coordinates because the window frame may be moving at the same time. + val deltaX = event.rawX - event.x + val deltaY = event.rawY - event.y + event.offsetLocation(deltaX, deltaY) + velocityTracker.addMovement(event) + event.offsetLocation(-deltaX, -deltaY) + } + + /** Animates sticking the object to the provided target with the given start velocities. */ + private fun animateStuckToTargetInternal( + target: MagneticTarget, + velX: Float, + velY: Float, + flung: Boolean, + after: (() -> Unit)? = null + ) { + target.updateLocationOnScreen() + getLocationOnScreen(underlyingObject, objectLocationOnScreen) + + // Calculate the difference between the target's center coordinates and the object's. + // Animating the object's x/y properties by these values will center the object on top + // of the magnetic target. + val xDiff = target.centerOnScreen.x - + getWidth(underlyingObject) / 2f - objectLocationOnScreen[0] + val yDiff = target.centerOnScreen.y - + getHeight(underlyingObject) / 2f - objectLocationOnScreen[1] + + val springConfig = if (flung) flungIntoTargetSpringConfig else springConfig + + cancelAnimations() + + // Animate to the center of the target. + animator + .spring(xProperty, xProperty.getValue(underlyingObject) + xDiff, velX, + springConfig) + .spring(yProperty, yProperty.getValue(underlyingObject) + yDiff, velY, + springConfig) + + if (physicsAnimatorUpdateListener != null) { + animator.addUpdateListener(physicsAnimatorUpdateListener!!) + } + + if (physicsAnimatorEndListener != null) { + animator.addEndListener(physicsAnimatorEndListener!!) + } + + if (after != null) { + animator.withEndActions(after) + } + + animator.start() + } + + /** + * Whether or not the provided values match a 'fast fling' towards the provided target. If it + * does, we consider it a fling-to-target gesture. + */ + private fun isForcefulFlingTowardsTarget( + target: MagneticTarget, + rawX: Float, + rawY: Float, + velX: Float, + velY: Float + ): Boolean { + if (!flingToTargetEnabled) { + return false + } + + // Whether velocity is sufficient, depending on whether we're flinging into a target at the + // top or the bottom of the screen. + val velocitySufficient = + if (rawY < target.centerOnScreen.y) velY > flingToTargetMinVelocity + else velY < flingToTargetMinVelocity + + if (!velocitySufficient) { + return false + } + + // Whether the trajectory of the fling intersects the target area. + var targetCenterXIntercept = rawX + + // Only do math if the X velocity is non-zero, otherwise X won't change. + if (velX != 0f) { + // Rise over run... + val slope = velY / velX + // ...y = mx + b, b = y / mx... + val yIntercept = rawY - slope * rawX + + // ...calculate the x value when y = the target's y-coordinate. + targetCenterXIntercept = (target.centerOnScreen.y - yIntercept) / slope + } + + // The width of the area we're looking for a fling towards. + val targetAreaWidth = target.targetView.width * flingToTargetWidthPercent + + // Velocity was sufficient, so return true if the intercept is within the target area. + return targetCenterXIntercept > target.centerOnScreen.x - targetAreaWidth / 2 && + targetCenterXIntercept < target.centerOnScreen.x + targetAreaWidth / 2 + } + + /** Cancel animations on this object's x/y properties. */ + internal fun cancelAnimations() { + animator.cancel(xProperty, yProperty) + } + + /** Updates the locations on screen of all of the [associatedTargets]. */ + internal fun updateTargetViews() { + associatedTargets.forEach { it.updateLocationOnScreen() } + + // Update the touch slop, since the configuration may have changed. + if (associatedTargets.size > 0) { + touchSlop = + ViewConfiguration.get(associatedTargets[0].targetView.context).scaledTouchSlop + } + } + + /** + * Represents a target view with a magnetic field radius and cached center-on-screen + * coordinates. + * + * Instances of MagneticTarget are passed to a MagnetizedObject's [addTarget], and can then + * attract the object if it's dragged near or flung towards it. MagneticTargets can be added to + * multiple objects. + */ + class MagneticTarget( + val targetView: View, + var magneticFieldRadiusPx: Int + ) { + val centerOnScreen = PointF() + + private val tempLoc = IntArray(2) + + fun updateLocationOnScreen() { + targetView.post { + targetView.getLocationOnScreen(tempLoc) + + // Add half of the target size to get the center, and subtract translation since the + // target could be animating in while we're doing this calculation. + centerOnScreen.set( + tempLoc[0] + targetView.width / 2f - targetView.translationX, + tempLoc[1] + targetView.height / 2f - targetView.translationY) + } + } + } + + companion object { + + /** + * Whether the HAPTIC_FEEDBACK_ENABLED setting is true. + * + * We put it in the companion object because we need to register a settings observer and + * [MagnetizedObject] doesn't have an obvious lifecycle so we don't have a good time to + * remove that observer. Since this settings is shared among all instances we just let all + * instances read from this value. + */ + private var systemHapticsEnabled = false + private var hapticSettingObserverInitialized = false + + private fun initHapticSettingObserver(context: Context) { + if (hapticSettingObserverInitialized) { + return + } + + val hapticSettingObserver = + object : ContentObserver(Handler.getMain()) { + override fun onChange(selfChange: Boolean) { + systemHapticsEnabled = + Settings.System.getIntForUser( + context.contentResolver, + Settings.System.HAPTIC_FEEDBACK_ENABLED, + 0, + UserHandle.USER_CURRENT) != 0 + } + } + + context.contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.HAPTIC_FEEDBACK_ENABLED), + true /* notifyForDescendants */, hapticSettingObserver) + + // Trigger the observer once to initialize systemHapticsEnabled. + hapticSettingObserver.onChange(false /* selfChange */) + hapticSettingObserverInitialized = true + } + + /** + * Magnetizes the given view. Magnetized views are attracted to one or more magnetic + * targets. Magnetic targets attract objects that are dragged near them, and hold them there + * unless they're moved away or released. Releasing objects inside a magnetic target + * typically performs an action on the object. + * + * Magnetized views can also be flung to targets, which will result in the view being pulled + * into the target and released as if it was dragged into it. + * + * To use the returned MagnetizedObject<View> instance, first set [magnetListener] to + * receive event callbacks. In your touch handler, pass all MotionEvents that move this view + * to [maybeConsumeMotionEvent]. If that method returns true, consider the event consumed by + * MagnetizedObject and don't move the view unless it begins returning false again. + * + * The view will be moved via translationX/Y properties, and its + * width/height will be determined via getWidth()/getHeight(). If you are animating + * something other than a view, or want to position your view using properties other than + * translationX/Y, implement an instance of [MagnetizedObject]. + * + * Note that the magnetic library can't re-order your view automatically. If the view + * renders on top of the target views, it will obscure the target when it sticks to it. + * You'll want to bring the view to the front in [MagnetListener.onStuckToTarget]. + */ + @JvmStatic + fun <T : View> magnetizeView(view: T): MagnetizedObject<T> { + return object : MagnetizedObject<T>( + view.context, + view, + DynamicAnimation.TRANSLATION_X, + DynamicAnimation.TRANSLATION_Y) { + override fun getWidth(underlyingObject: T): Float { + return underlyingObject.width.toFloat() + } + + override fun getHeight(underlyingObject: T): Float { + return underlyingObject.height.toFloat() } + + override fun getLocationOnScreen(underlyingObject: T, loc: IntArray) { + underlyingObject.getLocationOnScreen(loc) + } + } + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java new file mode 100644 index 000000000000..9bb709f9a82a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java @@ -0,0 +1,75 @@ +/* + * 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.onehanded; + +import androidx.annotation.NonNull; + +import com.android.wm.shell.onehanded.OneHandedGestureHandler.OneHandedGestureEventCallback; + +import java.io.PrintWriter; + +/** + * Interface to engage one handed feature. + */ +public interface OneHanded { + /** + * Return one handed settings enabled or not. + */ + boolean isOneHandedEnabled(); + + /** + * Return swipe to notification settings enabled or not. + */ + boolean isSwipeToNotificationEnabled(); + + /** + * Enters one handed mode. + */ + void startOneHanded(); + + /** + * Exits one handed mode. + */ + void stopOneHanded(); + + /** + * Exits one handed mode with {@link OneHandedEvents}. + */ + void stopOneHanded(int event); + + /** + * Set navigation 3 button mode enabled or disabled by users. + */ + void setThreeButtonModeEnabled(boolean enabled); + + /** + * Register callback to be notified after {@link OneHandedDisplayAreaOrganizer} + * transition start or finish + */ + void registerTransitionCallback(OneHandedTransitionCallback callback); + + /** + * Register callback for one handed gesture, this gesture callbcak will be activated on + * 3 button navigation mode only + */ + void registerGestureCallback(OneHandedGestureEventCallback callback); + + /** + * Dump one handed status. + */ + void dump(@NonNull PrintWriter pw); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationCallback.java new file mode 100644 index 000000000000..6749f7eec968 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationCallback.java @@ -0,0 +1,51 @@ +/* + * 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.onehanded; + +import android.view.SurfaceControl; + +/** + * Additional callback interface for OneHanded animation + */ +public interface OneHandedAnimationCallback { + /** + * Called when OneHanded animation is started. + */ + default void onOneHandedAnimationStart( + OneHandedAnimationController.OneHandedTransitionAnimator animator) { + } + + /** + * Called when OneHanded animation is ended. + */ + default void onOneHandedAnimationEnd(SurfaceControl.Transaction tx, + OneHandedAnimationController.OneHandedTransitionAnimator animator) { + } + + /** + * Called when OneHanded animation is cancelled. + */ + default void onOneHandedAnimationCancel( + OneHandedAnimationController.OneHandedTransitionAnimator animator) { + } + + /** + * Called when OneHanded animator is updating offset + */ + default void onTutorialAnimationUpdate(int offset) {} + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationController.java new file mode 100644 index 000000000000..963909621a1b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationController.java @@ -0,0 +1,302 @@ +/* + * 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.onehanded; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.annotation.IntDef; +import android.content.Context; +import android.graphics.Rect; +import android.view.SurfaceControl; +import android.view.animation.Interpolator; +import android.view.animation.OvershootInterpolator; + +import androidx.annotation.VisibleForTesting; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * Controller class of OneHanded animations (both from and to OneHanded mode). + */ +public class OneHandedAnimationController { + private static final float FRACTION_START = 0f; + private static final float FRACTION_END = 1f; + + public static final int TRANSITION_DIRECTION_NONE = 0; + public static final int TRANSITION_DIRECTION_TRIGGER = 1; + public static final int TRANSITION_DIRECTION_EXIT = 2; + + @IntDef(prefix = {"TRANSITION_DIRECTION_"}, value = { + TRANSITION_DIRECTION_NONE, + TRANSITION_DIRECTION_TRIGGER, + TRANSITION_DIRECTION_EXIT, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface TransitionDirection { + } + + private final Interpolator mOvershootInterpolator; + private final OneHandedSurfaceTransactionHelper mSurfaceTransactionHelper; + private final HashMap<SurfaceControl, OneHandedTransitionAnimator> mAnimatorMap = + new HashMap<>(); + + /** + * Constructor of OneHandedAnimationController + */ + public OneHandedAnimationController(Context context) { + mSurfaceTransactionHelper = new OneHandedSurfaceTransactionHelper(context); + mOvershootInterpolator = new OvershootInterpolator(); + } + + @SuppressWarnings("unchecked") + OneHandedTransitionAnimator getAnimator(SurfaceControl leash, Rect startBounds, + Rect endBounds) { + final OneHandedTransitionAnimator animator = mAnimatorMap.get(leash); + if (animator == null) { + mAnimatorMap.put(leash, setupOneHandedTransitionAnimator( + OneHandedTransitionAnimator.ofBounds(leash, startBounds, endBounds))); + } else if (animator.isRunning()) { + animator.updateEndValue(endBounds); + } else { + animator.cancel(); + mAnimatorMap.put(leash, setupOneHandedTransitionAnimator( + OneHandedTransitionAnimator.ofBounds(leash, startBounds, endBounds))); + } + return mAnimatorMap.get(leash); + } + + HashMap<SurfaceControl, OneHandedTransitionAnimator> getAnimatorMap() { + return mAnimatorMap; + } + + boolean isAnimatorsConsumed() { + return mAnimatorMap.isEmpty(); + } + + void removeAnimator(SurfaceControl key) { + final OneHandedTransitionAnimator animator = mAnimatorMap.remove(key); + if (animator != null && animator.isRunning()) { + animator.cancel(); + } + } + + OneHandedTransitionAnimator setupOneHandedTransitionAnimator( + OneHandedTransitionAnimator animator) { + animator.setSurfaceTransactionHelper(mSurfaceTransactionHelper); + animator.setInterpolator(mOvershootInterpolator); + animator.setFloatValues(FRACTION_START, FRACTION_END); + return animator; + } + + /** + * Animator for OneHanded transition animation which supports both alpha and bounds animation. + * + * @param <T> Type of property to animate, either offset (float) + */ + public abstract static class OneHandedTransitionAnimator<T> extends ValueAnimator implements + ValueAnimator.AnimatorUpdateListener, + ValueAnimator.AnimatorListener { + + private final SurfaceControl mLeash; + private T mStartValue; + private T mEndValue; + private T mCurrentValue; + + private final List<OneHandedAnimationCallback> mOneHandedAnimationCallbacks = + new ArrayList<>(); + private OneHandedSurfaceTransactionHelper mSurfaceTransactionHelper; + private OneHandedSurfaceTransactionHelper.SurfaceControlTransactionFactory + mSurfaceControlTransactionFactory; + + private @TransitionDirection int mTransitionDirection; + private int mTransitionOffset; + + private OneHandedTransitionAnimator(SurfaceControl leash, T startValue, T endValue) { + mLeash = leash; + mStartValue = startValue; + mEndValue = endValue; + addListener(this); + addUpdateListener(this); + mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; + mTransitionDirection = TRANSITION_DIRECTION_NONE; + } + + @Override + public void onAnimationStart(Animator animation) { + mCurrentValue = mStartValue; + mOneHandedAnimationCallbacks.forEach( + (callback) -> { + callback.onOneHandedAnimationStart(this); + } + ); + } + + @Override + public void onAnimationEnd(Animator animation) { + mCurrentValue = mEndValue; + final SurfaceControl.Transaction tx = newSurfaceControlTransaction(); + onEndTransaction(mLeash, tx); + mOneHandedAnimationCallbacks.forEach( + (callback) -> { + callback.onOneHandedAnimationEnd(tx, this); + } + ); + } + + @Override + public void onAnimationCancel(Animator animation) { + mCurrentValue = mEndValue; + mOneHandedAnimationCallbacks.forEach( + (callback) -> { + callback.onOneHandedAnimationCancel(this); + } + ); + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + applySurfaceControlTransaction(mLeash, newSurfaceControlTransaction(), + animation.getAnimatedFraction()); + mOneHandedAnimationCallbacks.forEach( + (callback) -> { + callback.onTutorialAnimationUpdate(((Rect) mCurrentValue).top); + } + ); + } + + void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) { + } + + void onEndTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) { + } + + abstract void applySurfaceControlTransaction(SurfaceControl leash, + SurfaceControl.Transaction tx, float fraction); + + OneHandedSurfaceTransactionHelper getSurfaceTransactionHelper() { + return mSurfaceTransactionHelper; + } + + void setSurfaceTransactionHelper(OneHandedSurfaceTransactionHelper helper) { + mSurfaceTransactionHelper = helper; + } + + OneHandedTransitionAnimator<T> setOneHandedAnimationCallbacks( + OneHandedAnimationCallback callback) { + mOneHandedAnimationCallbacks.add(callback); + return this; + } + + SurfaceControl getLeash() { + return mLeash; + } + + Rect getDestinationBounds() { + return (Rect) mEndValue; + } + + int getDestinationOffset() { + return ((Rect) mEndValue).top - ((Rect) mStartValue).top; + } + + @TransitionDirection + int getTransitionDirection() { + return mTransitionDirection; + } + + OneHandedTransitionAnimator<T> setTransitionDirection(int direction) { + mTransitionDirection = direction; + return this; + } + + OneHandedTransitionAnimator<T> setTransitionOffset(int offset) { + mTransitionOffset = offset; + return this; + } + + T getStartValue() { + return mStartValue; + } + + T getEndValue() { + return mEndValue; + } + + void setCurrentValue(T value) { + mCurrentValue = value; + } + + /** + * Updates the {@link #mEndValue}. + */ + void updateEndValue(T endValue) { + mEndValue = endValue; + } + + SurfaceControl.Transaction newSurfaceControlTransaction() { + return mSurfaceControlTransactionFactory.getTransaction(); + } + + @VisibleForTesting + static OneHandedTransitionAnimator<Rect> ofBounds(SurfaceControl leash, + Rect startValue, Rect endValue) { + + return new OneHandedTransitionAnimator<Rect>(leash, new Rect(startValue), + new Rect(endValue)) { + + private final Rect mTmpRect = new Rect(); + + private int getCastedFractionValue(float start, float end, float fraction) { + return (int) (start * (1 - fraction) + end * fraction + .5f); + } + + @Override + void applySurfaceControlTransaction(SurfaceControl leash, + SurfaceControl.Transaction tx, float fraction) { + final Rect start = getStartValue(); + final Rect end = getEndValue(); + mTmpRect.set( + getCastedFractionValue(start.left, end.left, fraction), + getCastedFractionValue(start.top, end.top, fraction), + getCastedFractionValue(start.right, end.right, fraction), + getCastedFractionValue(start.bottom, end.bottom, fraction)); + setCurrentValue(mTmpRect); + getSurfaceTransactionHelper().crop(tx, leash, mTmpRect) + .round(tx, leash); + tx.apply(); + } + + @Override + void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) { + getSurfaceTransactionHelper() + .alpha(tx, leash, 1f) + .translate(tx, leash, getEndValue().top - getStartValue().top) + .round(tx, leash); + tx.apply(); + } + }; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java new file mode 100644 index 000000000000..7d039d4d92f6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java @@ -0,0 +1,431 @@ +/* + * 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.onehanded; + +import static android.os.UserHandle.USER_CURRENT; +import static android.view.Display.DEFAULT_DISPLAY; + +import android.content.Context; +import android.content.om.IOverlayManager; +import android.content.om.OverlayInfo; +import android.database.ContentObserver; +import android.graphics.Point; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemProperties; +import android.provider.Settings; +import android.util.Slog; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.wm.shell.common.DisplayChangeController; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.onehanded.OneHandedGestureHandler.OneHandedGestureEventCallback; + +import java.io.PrintWriter; + +/** + * Manages and manipulates the one handed states, transitions, and gesture for phones. + */ +public class OneHandedController implements OneHanded { + private static final String TAG = "OneHandedController"; + + private static final String ONE_HANDED_MODE_OFFSET_PERCENTAGE = + "persist.debug.one_handed_offset_percentage"; + private static final String ONE_HANDED_MODE_GESTURAL_OVERLAY = + "com.android.internal.systemui.onehanded.gestural"; + + static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode"; + + private boolean mIsOneHandedEnabled; + private boolean mIsSwipeToNotificationEnabled; + private boolean mTaskChangeToExit; + private float mOffSetFraction; + + private final Context mContext; + private final DisplayController mDisplayController; + private final OneHandedGestureHandler mGestureHandler; + private final OneHandedTimeoutHandler mTimeoutHandler; + private final OneHandedTouchHandler mTouchHandler; + private final OneHandedTutorialHandler mTutorialHandler; + private final IOverlayManager mOverlayManager; + private final Handler mMainHandler = new Handler(Looper.getMainLooper()); + + private OneHandedDisplayAreaOrganizer mDisplayAreaOrganizer; + + /** + * Handle rotation based on OnDisplayChangingListener callback + */ + private final DisplayChangeController.OnDisplayChangingListener mRotationController = + (display, fromRotation, toRotation, wct) -> { + if (mDisplayAreaOrganizer != null) { + mDisplayAreaOrganizer.onRotateDisplay(fromRotation, toRotation, wct); + } + }; + + private final ContentObserver mEnabledObserver = new ContentObserver(mMainHandler) { + @Override + public void onChange(boolean selfChange) { + final boolean enabled = OneHandedSettingsUtil.getSettingsOneHandedModeEnabled( + mContext.getContentResolver()); + OneHandedEvents.writeEvent(enabled + ? OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_ENABLED_ON + : OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_ENABLED_OFF); + + setOneHandedEnabled(enabled); + + // Also checks swipe to notification settings since they all need gesture overlay. + setEnabledGesturalOverlay( + enabled || OneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled( + mContext.getContentResolver())); + } + }; + + private final ContentObserver mTimeoutObserver = new ContentObserver(mMainHandler) { + @Override + public void onChange(boolean selfChange) { + final int newTimeout = OneHandedSettingsUtil.getSettingsOneHandedModeTimeout( + mContext.getContentResolver()); + int metricsId = OneHandedEvents.OneHandedSettingsTogglesEvent.INVALID.getId(); + switch (newTimeout) { + case OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER: + metricsId = OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_NEVER; + break; + case OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS: + metricsId = OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_4; + break; + case OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS: + metricsId = OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_8; + break; + case OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_LONG_IN_SECONDS: + metricsId = OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_12; + break; + default: + // do nothing + break; + } + OneHandedEvents.writeEvent(metricsId); + + if (mTimeoutHandler != null) { + mTimeoutHandler.setTimeout(newTimeout); + } + } + }; + + private final ContentObserver mTaskChangeExitObserver = new ContentObserver(mMainHandler) { + @Override + public void onChange(boolean selfChange) { + final boolean enabled = OneHandedSettingsUtil.getSettingsTapsAppToExit( + mContext.getContentResolver()); + OneHandedEvents.writeEvent(enabled + ? OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_ON + : OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_OFF); + + setTaskChangeToExit(enabled); + } + }; + + private final ContentObserver mSwipeToNotificationEnabledObserver = + new ContentObserver(mMainHandler) { + @Override + public void onChange(boolean selfChange) { + final boolean enabled = + OneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled( + mContext.getContentResolver()); + setSwipeToNotificationEnabled(enabled); + + // Also checks one handed mode settings since they all need gesture overlay. + setEnabledGesturalOverlay( + enabled || OneHandedSettingsUtil.getSettingsOneHandedModeEnabled( + mContext.getContentResolver())); + } + }; + + /** + * Creates {@link OneHandedController}, returns {@code null} if the feature is not supported. + */ + @Nullable + public static OneHandedController create( + Context context, DisplayController displayController) { + if (!SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false)) { + Slog.w(TAG, "Device doesn't support OneHanded feature"); + return null; + } + + OneHandedTutorialHandler tutorialHandler = new OneHandedTutorialHandler(context); + OneHandedAnimationController animationController = + new OneHandedAnimationController(context); + OneHandedTouchHandler touchHandler = new OneHandedTouchHandler(); + OneHandedGestureHandler gestureHandler = new OneHandedGestureHandler( + context, displayController); + OneHandedDisplayAreaOrganizer organizer = new OneHandedDisplayAreaOrganizer( + context, displayController, animationController, tutorialHandler); + IOverlayManager overlayManager = IOverlayManager.Stub.asInterface( + ServiceManager.getService(Context.OVERLAY_SERVICE)); + return new OneHandedController(context, displayController, organizer, touchHandler, + tutorialHandler, gestureHandler, overlayManager); + } + + @VisibleForTesting + OneHandedController(Context context, + DisplayController displayController, + OneHandedDisplayAreaOrganizer displayAreaOrganizer, + OneHandedTouchHandler touchHandler, + OneHandedTutorialHandler tutorialHandler, + OneHandedGestureHandler gestureHandler, + IOverlayManager overlayManager) { + mContext = context; + mDisplayAreaOrganizer = displayAreaOrganizer; + mDisplayController = displayController; + mTouchHandler = touchHandler; + mTutorialHandler = tutorialHandler; + mGestureHandler = gestureHandler; + mOverlayManager = overlayManager; + + mOffSetFraction = SystemProperties.getInt(ONE_HANDED_MODE_OFFSET_PERCENTAGE, 50) + / 100.0f; + mIsOneHandedEnabled = OneHandedSettingsUtil.getSettingsOneHandedModeEnabled( + context.getContentResolver()); + mIsSwipeToNotificationEnabled = + OneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled( + context.getContentResolver()); + mTimeoutHandler = OneHandedTimeoutHandler.get(); + + mDisplayController.addDisplayChangingController(mRotationController); + + setupCallback(); + setupSettingObservers(); + setupTimeoutListener(); + setupGesturalOverlay(); + updateSettings(); + } + + /** + * Set one handed enabled or disabled when user update settings + */ + void setOneHandedEnabled(boolean enabled) { + mIsOneHandedEnabled = enabled; + updateOneHandedEnabled(); + } + + /** + * Set one handed enabled or disabled by when user update settings + */ + void setTaskChangeToExit(boolean enabled) { + mTaskChangeToExit = enabled; + } + + /** + * Sets whether to enable swipe bottom to notification gesture when user update settings. + */ + void setSwipeToNotificationEnabled(boolean enabled) { + mIsSwipeToNotificationEnabled = enabled; + updateOneHandedEnabled(); + } + + @Override + public boolean isOneHandedEnabled() { + return mIsOneHandedEnabled; + } + + @Override + public boolean isSwipeToNotificationEnabled() { + return mIsSwipeToNotificationEnabled; + } + + @Override + public void startOneHanded() { + if (!mDisplayAreaOrganizer.isInOneHanded()) { + final int yOffSet = Math.round(getDisplaySize().y * mOffSetFraction); + mDisplayAreaOrganizer.scheduleOffset(0, yOffSet); + mTimeoutHandler.resetTimer(); + + OneHandedEvents.writeEvent(OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_GESTURE_IN); + } + } + + @Override + public void stopOneHanded() { + if (mDisplayAreaOrganizer.isInOneHanded()) { + mDisplayAreaOrganizer.scheduleOffset(0, 0); + mTimeoutHandler.removeTimer(); + } + } + + @Override + public void stopOneHanded(int event) { + if (!mTaskChangeToExit && event == OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_APP_TAPS_OUT) { + //Task change exit not enable, do nothing and return here. + return; + } + + if (mDisplayAreaOrganizer.isInOneHanded()) { + OneHandedEvents.writeEvent(event); + } + + stopOneHanded(); + } + + @Override + public void setThreeButtonModeEnabled(boolean enabled) { + mGestureHandler.onThreeButtonModeEnabled(enabled); + } + + @Override + public void registerTransitionCallback(OneHandedTransitionCallback callback) { + mDisplayAreaOrganizer.registerTransitionCallback(callback); + } + + @Override + public void registerGestureCallback(OneHandedGestureEventCallback callback) { + mGestureHandler.setGestureEventListener(callback); + } + + private void setupCallback() { + mTouchHandler.registerTouchEventListener(() -> + stopOneHanded(OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_OVERSPACE_OUT)); + mDisplayAreaOrganizer.registerTransitionCallback(mTouchHandler); + mDisplayAreaOrganizer.registerTransitionCallback(mGestureHandler); + mDisplayAreaOrganizer.registerTransitionCallback(mTutorialHandler); + } + + private void setupSettingObservers() { + OneHandedSettingsUtil.registerSettingsKeyObserver(Settings.Secure.ONE_HANDED_MODE_ENABLED, + mContext.getContentResolver(), mEnabledObserver); + OneHandedSettingsUtil.registerSettingsKeyObserver(Settings.Secure.ONE_HANDED_MODE_TIMEOUT, + mContext.getContentResolver(), mTimeoutObserver); + OneHandedSettingsUtil.registerSettingsKeyObserver(Settings.Secure.TAPS_APP_TO_EXIT, + mContext.getContentResolver(), mTaskChangeExitObserver); + OneHandedSettingsUtil.registerSettingsKeyObserver( + Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED, + mContext.getContentResolver(), mSwipeToNotificationEnabledObserver); + } + + private void updateSettings() { + setOneHandedEnabled(OneHandedSettingsUtil + .getSettingsOneHandedModeEnabled(mContext.getContentResolver())); + mTimeoutHandler.setTimeout(OneHandedSettingsUtil + .getSettingsOneHandedModeTimeout(mContext.getContentResolver())); + setTaskChangeToExit(OneHandedSettingsUtil + .getSettingsTapsAppToExit(mContext.getContentResolver())); + setSwipeToNotificationEnabled(OneHandedSettingsUtil + .getSettingsSwipeToNotificationEnabled(mContext.getContentResolver())); + } + + private void setupTimeoutListener() { + mTimeoutHandler.registerTimeoutListener(timeoutTime -> { + stopOneHanded(OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_TIMEOUT_OUT); + }); + } + + /** + * Query the current display real size from {@link DisplayController} + * + * @return {@link DisplayController#getDisplay(int)#getDisplaySize()} + */ + private Point getDisplaySize() { + Point displaySize = new Point(); + if (mDisplayController != null && mDisplayController.getDisplay(DEFAULT_DISPLAY) != null) { + mDisplayController.getDisplay(DEFAULT_DISPLAY).getRealSize(displaySize); + } + return displaySize; + } + + private void updateOneHandedEnabled() { + if (mDisplayAreaOrganizer.isInOneHanded()) { + stopOneHanded(); + } + // TODO Be aware to unregisterOrganizer() after animation finished + mDisplayAreaOrganizer.unregisterOrganizer(); + if (mIsOneHandedEnabled) { + mDisplayAreaOrganizer.registerOrganizer( + OneHandedDisplayAreaOrganizer.FEATURE_ONE_HANDED); + } + mTouchHandler.onOneHandedEnabled(mIsOneHandedEnabled); + mGestureHandler.onOneHandedEnabled(mIsOneHandedEnabled || mIsSwipeToNotificationEnabled); + } + + private void setupGesturalOverlay() { + if (!OneHandedSettingsUtil.getSettingsOneHandedModeEnabled(mContext.getContentResolver())) { + return; + } + + OverlayInfo info = null; + try { + // TODO(b/157958539) migrate new RRO config file after S+ + mOverlayManager.setHighestPriority(ONE_HANDED_MODE_GESTURAL_OVERLAY, USER_CURRENT); + info = mOverlayManager.getOverlayInfo(ONE_HANDED_MODE_GESTURAL_OVERLAY, USER_CURRENT); + } catch (RemoteException e) { /* Do nothing */ } + + if (info != null && !info.isEnabled()) { + // Enable the default gestural one handed overlay. + setEnabledGesturalOverlay(true); + } + } + + @VisibleForTesting + private void setEnabledGesturalOverlay(boolean enabled) { + try { + mOverlayManager.setEnabled(ONE_HANDED_MODE_GESTURAL_OVERLAY, enabled, USER_CURRENT); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @Override + public void dump(@NonNull PrintWriter pw) { + final String innerPrefix = " "; + pw.println(TAG + "states: "); + pw.print(innerPrefix + "mOffSetFraction="); + pw.println(mOffSetFraction); + + if (mDisplayAreaOrganizer != null) { + mDisplayAreaOrganizer.dump(pw); + } + + if (mTouchHandler != null) { + mTouchHandler.dump(pw); + } + + if (mTimeoutHandler != null) { + mTimeoutHandler.dump(pw); + } + + if (mTutorialHandler != null) { + mTutorialHandler.dump(pw); + } + + OneHandedSettingsUtil.dump(pw, innerPrefix, mContext.getContentResolver()); + + if (mOverlayManager != null) { + OverlayInfo info = null; + try { + info = mOverlayManager.getOverlayInfo(ONE_HANDED_MODE_GESTURAL_OVERLAY, + USER_CURRENT); + } catch (RemoteException e) { /* Do nothing */ } + + if (info != null && !info.isEnabled()) { + pw.print(innerPrefix + "OverlayInfo="); + pw.println(info); + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java new file mode 100644 index 000000000000..17418f934691 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java @@ -0,0 +1,377 @@ +/* + * 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.onehanded; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.wm.shell.onehanded.OneHandedAnimationController.TRANSITION_DIRECTION_EXIT; +import static com.android.wm.shell.onehanded.OneHandedAnimationController.TRANSITION_DIRECTION_TRIGGER; + +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemProperties; +import android.util.ArrayMap; +import android.util.Log; +import android.view.SurfaceControl; +import android.window.DisplayAreaInfo; +import android.window.DisplayAreaOrganizer; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.internal.os.SomeArgs; +import com.android.wm.shell.common.DisplayController; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Manages OneHanded display areas such as offset. + * + * This class listens on {@link DisplayAreaOrganizer} callbacks for windowing mode change + * both to and from OneHanded and issues corresponding animation if applicable. + * Normally, we apply series of {@link SurfaceControl.Transaction} when the animator is running + * and files a final {@link WindowContainerTransaction} at the end of the transition. + * + * This class is also responsible for translating one handed operations within SysUI component + */ +public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { + private static final String TAG = "OneHandedDisplayAreaOrganizer"; + private static final String ONE_HANDED_MODE_TRANSLATE_ANIMATION_DURATION = + "persist.debug.one_handed_translate_animation_duration"; + + @VisibleForTesting + static final int MSG_RESET_IMMEDIATE = 1; + @VisibleForTesting + static final int MSG_OFFSET_ANIMATE = 2; + @VisibleForTesting + static final int MSG_OFFSET_FINISH = 3; + + private final Rect mLastVisualDisplayBounds = new Rect(); + private final Rect mDefaultDisplayBounds = new Rect(); + + private Handler mUpdateHandler; + private boolean mIsInOneHanded; + private int mEnterExitAnimationDurationMs; + + @VisibleForTesting + ArrayMap<DisplayAreaInfo, SurfaceControl> mDisplayAreaMap = new ArrayMap(); + private DisplayController mDisplayController; + private OneHandedAnimationController mAnimationController; + private OneHandedSurfaceTransactionHelper.SurfaceControlTransactionFactory + mSurfaceControlTransactionFactory; + private OneHandedTutorialHandler mTutorialHandler; + private List<OneHandedTransitionCallback> mTransitionCallbacks = new ArrayList<>(); + + @VisibleForTesting + OneHandedAnimationCallback mOneHandedAnimationCallback = + new OneHandedAnimationCallback() { + @Override + public void onOneHandedAnimationStart( + OneHandedAnimationController.OneHandedTransitionAnimator animator) { + } + + @Override + public void onOneHandedAnimationEnd(SurfaceControl.Transaction tx, + OneHandedAnimationController.OneHandedTransitionAnimator animator) { + mAnimationController.removeAnimator(animator.getLeash()); + if (mAnimationController.isAnimatorsConsumed()) { + mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_OFFSET_FINISH, + obtainArgsFromAnimator(animator))); + } + } + + @Override + public void onOneHandedAnimationCancel( + OneHandedAnimationController.OneHandedTransitionAnimator animator) { + mAnimationController.removeAnimator(animator.getLeash()); + if (mAnimationController.isAnimatorsConsumed()) { + mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_OFFSET_FINISH, + obtainArgsFromAnimator(animator))); + } + } + }; + + @SuppressWarnings("unchecked") + private Handler.Callback mUpdateCallback = (msg) -> { + SomeArgs args = (SomeArgs) msg.obj; + final Rect currentBounds = args.arg1 != null ? (Rect) args.arg1 : mDefaultDisplayBounds; + final WindowContainerTransaction wctFromRotate = (WindowContainerTransaction) args.arg2; + final int yOffset = args.argi2; + final int direction = args.argi3; + + switch (msg.what) { + case MSG_RESET_IMMEDIATE: + resetWindowsOffset(wctFromRotate); + mDefaultDisplayBounds.set(currentBounds); + mLastVisualDisplayBounds.set(currentBounds); + finishOffset(0, TRANSITION_DIRECTION_EXIT); + break; + case MSG_OFFSET_ANIMATE: + final Rect toBounds = new Rect(mDefaultDisplayBounds.left, + mDefaultDisplayBounds.top + yOffset, + mDefaultDisplayBounds.right, + mDefaultDisplayBounds.bottom + yOffset); + offsetWindows(currentBounds, toBounds, direction, mEnterExitAnimationDurationMs); + break; + case MSG_OFFSET_FINISH: + finishOffset(yOffset, direction); + break; + } + args.recycle(); + return true; + }; + + /** + * Constructor of OneHandedDisplayAreaOrganizer + */ + public OneHandedDisplayAreaOrganizer(Context context, + DisplayController displayController, + OneHandedAnimationController animationController, + OneHandedTutorialHandler tutorialHandler) { + mUpdateHandler = new Handler(OneHandedThread.get().getLooper(), mUpdateCallback); + mAnimationController = animationController; + mDisplayController = displayController; + mDefaultDisplayBounds.set(getDisplayBounds()); + mLastVisualDisplayBounds.set(getDisplayBounds()); + mEnterExitAnimationDurationMs = + SystemProperties.getInt(ONE_HANDED_MODE_TRANSLATE_ANIMATION_DURATION, 300); + mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; + mTutorialHandler = tutorialHandler; + } + + @Override + public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo, + @NonNull SurfaceControl leash) { + Objects.requireNonNull(displayAreaInfo, "displayAreaInfo must not be null"); + Objects.requireNonNull(leash, "leash must not be null"); + synchronized (this) { + if (mDisplayAreaMap.get(displayAreaInfo) == null) { + // mDefaultDisplayBounds may out of date after removeDisplayChangingController() + mDefaultDisplayBounds.set(getDisplayBounds()); + mDisplayAreaMap.put(displayAreaInfo, leash); + } + } + } + + @Override + public void onDisplayAreaVanished(@NonNull DisplayAreaInfo displayAreaInfo) { + Objects.requireNonNull(displayAreaInfo, + "Requires valid displayArea, and displayArea must not be null"); + synchronized (this) { + if (!mDisplayAreaMap.containsKey(displayAreaInfo)) { + Log.w(TAG, "Unrecognized token: " + displayAreaInfo.token); + return; + } + mDisplayAreaMap.remove(displayAreaInfo); + } + } + + @Override + public void unregisterOrganizer() { + super.unregisterOrganizer(); + mUpdateHandler.post(() -> resetWindowsOffset(null)); + } + + /** + * Handler for display rotation changes by below policy which + * handles 90 degree display rotation changes {@link Surface.Rotation}. + * + * @param fromRotation starting rotation of the display. + * @param toRotation target rotation of the display (after rotating). + * @param wct A task transaction {@link WindowContainerTransaction} from + * {@link DisplayChangeController} to populate. + */ + public void onRotateDisplay(int fromRotation, int toRotation, WindowContainerTransaction wct) { + // Stop one handed without animation and reset cropped size immediately + final Rect newBounds = new Rect(mDefaultDisplayBounds); + final boolean isOrientationDiff = Math.abs(fromRotation - toRotation) % 2 == 1; + + if (isOrientationDiff) { + newBounds.set(newBounds.left, newBounds.top, newBounds.bottom, newBounds.right); + SomeArgs args = SomeArgs.obtain(); + args.arg1 = newBounds; + args.arg2 = wct; + args.argi1 = 0 /* xOffset */; + args.argi2 = 0 /* yOffset */; + args.argi3 = TRANSITION_DIRECTION_EXIT; + mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESET_IMMEDIATE, args)); + } + } + + /** + * Offset the windows by a given offset on Y-axis, triggered also from screen rotation. + * Directly perform manipulation/offset on the leash. + */ + public void scheduleOffset(int xOffset, int yOffset) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = getLastVisualDisplayBounds(); + args.argi1 = xOffset; + args.argi2 = yOffset; + args.argi3 = yOffset > 0 ? TRANSITION_DIRECTION_TRIGGER : TRANSITION_DIRECTION_EXIT; + mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_OFFSET_ANIMATE, args)); + } + + private void offsetWindows(Rect fromBounds, Rect toBounds, int direction, int durationMs) { + if (Looper.myLooper() != mUpdateHandler.getLooper()) { + throw new RuntimeException("Callers should call scheduleOffset() instead of this " + + "directly"); + } + synchronized (this) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mDisplayAreaMap.forEach( + (key, leash) -> { + animateWindows(leash, fromBounds, toBounds, direction, durationMs); + wct.setBounds(key.token, toBounds); + }); + applyTransaction(wct); + } + } + + private void resetWindowsOffset(WindowContainerTransaction wct) { + synchronized (this) { + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); + mDisplayAreaMap.forEach( + (key, leash) -> { + final OneHandedAnimationController.OneHandedTransitionAnimator animator = + mAnimationController.getAnimatorMap().remove(leash); + if (animator != null && animator.isRunning()) { + animator.cancel(); + } + tx.setPosition(leash, 0, 0) + .setWindowCrop(leash, -1/* reset */, -1/* reset */); + // DisplayRotationController will applyTransaction() after finish rotating + if (wct != null) { + wct.setBounds(key.token, null/* reset */); + } + }); + tx.apply(); + } + } + + private void animateWindows(SurfaceControl leash, Rect fromBounds, Rect toBounds, + @OneHandedAnimationController.TransitionDirection int direction, int durationMs) { + if (Looper.myLooper() != mUpdateHandler.getLooper()) { + throw new RuntimeException("Callers should call scheduleOffset() instead of " + + "this directly"); + } + mUpdateHandler.post(() -> { + final OneHandedAnimationController.OneHandedTransitionAnimator animator = + mAnimationController.getAnimator(leash, fromBounds, toBounds); + if (animator != null) { + animator.setTransitionDirection(direction) + .setOneHandedAnimationCallbacks(mOneHandedAnimationCallback) + .setOneHandedAnimationCallbacks(mTutorialHandler.getAnimationCallback()) + .setDuration(durationMs) + .start(); + } + }); + } + + private void finishOffset(int offset, + @OneHandedAnimationController.TransitionDirection int direction) { + if (Looper.myLooper() != mUpdateHandler.getLooper()) { + throw new RuntimeException( + "Callers should call scheduleOffset() instead of this directly."); + } + // Only finishOffset() can update mIsInOneHanded to ensure the state is handle in sequence, + // the flag *MUST* be updated before dispatch mTransitionCallbacks + mIsInOneHanded = (offset > 0 || direction == TRANSITION_DIRECTION_TRIGGER); + mLastVisualDisplayBounds.offsetTo(0, + direction == TRANSITION_DIRECTION_TRIGGER ? offset : 0); + for (int i = mTransitionCallbacks.size() - 1; i >= 0; i--) { + final OneHandedTransitionCallback callback = mTransitionCallbacks.get(i); + if (direction == TRANSITION_DIRECTION_TRIGGER) { + callback.onStartFinished(getLastVisualDisplayBounds()); + } else { + callback.onStopFinished(getLastVisualDisplayBounds()); + } + } + } + + /** + * The latest state of one handed mode + * + * @return true Currently is in one handed mode, otherwise is not in one handed mode + */ + public boolean isInOneHanded() { + return mIsInOneHanded; + } + + /** + * The latest visual bounds of displayArea translated + * + * @return Rect latest finish_offset + */ + public Rect getLastVisualDisplayBounds() { + return mLastVisualDisplayBounds; + } + + @Nullable + private Rect getDisplayBounds() { + Point realSize = new Point(0, 0); + if (mDisplayController != null && mDisplayController.getDisplay(DEFAULT_DISPLAY) != null) { + mDisplayController.getDisplay(DEFAULT_DISPLAY).getRealSize(realSize); + } + return new Rect(0, 0, realSize.x, realSize.y); + } + + @VisibleForTesting + void setUpdateHandler(Handler updateHandler) { + mUpdateHandler = updateHandler; + } + + /** + * Register transition callback + */ + public void registerTransitionCallback(OneHandedTransitionCallback callback) { + mTransitionCallbacks.add(callback); + } + + private SomeArgs obtainArgsFromAnimator( + OneHandedAnimationController.OneHandedTransitionAnimator animator) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = animator.getDestinationBounds(); + args.argi1 = 0 /* xOffset */; + args.argi2 = animator.getDestinationOffset(); + args.argi3 = animator.getTransitionDirection(); + return args; + } + + void dump(@NonNull PrintWriter pw) { + final String innerPrefix = " "; + pw.println(TAG + "states: "); + pw.print(innerPrefix + "mIsInOneHanded="); + pw.println(mIsInOneHanded); + pw.print(innerPrefix + "mDisplayAreaMap="); + pw.println(mDisplayAreaMap); + pw.print(innerPrefix + "mDefaultDisplayBounds="); + pw.println(mDefaultDisplayBounds); + pw.print(innerPrefix + "mLastVisualDisplayBounds="); + pw.println(mLastVisualDisplayBounds); + pw.print(innerPrefix + "getDisplayBounds()="); + pw.println(getDisplayBounds()); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedEvents.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedEvents.java new file mode 100644 index 000000000000..79ddd2b11e72 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedEvents.java @@ -0,0 +1,276 @@ +/* + * 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.onehanded; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; +import com.android.internal.logging.UiEventLoggerImpl; + +/** + * Interesting events related to the One-Handed. + */ +public class OneHandedEvents { + private static final String TAG = "OneHandedEvents"; + + public static Callback sCallback; + @VisibleForTesting + static UiEventLogger sUiEventLogger = new UiEventLoggerImpl(); + + /** + * One-Handed event types + */ + // Triggers + public static final int EVENT_ONE_HANDED_TRIGGER_GESTURE_IN = 0; + public static final int EVENT_ONE_HANDED_TRIGGER_GESTURE_OUT = 1; + public static final int EVENT_ONE_HANDED_TRIGGER_OVERSPACE_OUT = 2; + public static final int EVENT_ONE_HANDED_TRIGGER_POP_IME_OUT = 3; + public static final int EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT = 4; + public static final int EVENT_ONE_HANDED_TRIGGER_APP_TAPS_OUT = 5; + public static final int EVENT_ONE_HANDED_TRIGGER_TIMEOUT_OUT = 6; + public static final int EVENT_ONE_HANDED_TRIGGER_SCREEN_OFF_OUT = 7; + // Settings toggles + public static final int EVENT_ONE_HANDED_SETTINGS_ENABLED_ON = 8; + public static final int EVENT_ONE_HANDED_SETTINGS_ENABLED_OFF = 9; + public static final int EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_ON = 10; + public static final int EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_OFF = 11; + public static final int EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_ON = 12; + public static final int EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_OFF = 13; + public static final int EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_NEVER = 14; + public static final int EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_4 = 15; + public static final int EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_8 = 16; + public static final int EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_12 = 17; + + private static final String[] EVENT_TAGS = { + "one_handed_trigger_gesture_in", + "one_handed_trigger_gesture_out", + "one_handed_trigger_overspace_out", + "one_handed_trigger_pop_ime_out", + "one_handed_trigger_rotation_out", + "one_handed_trigger_app_taps_out", + "one_handed_trigger_timeout_out", + "one_handed_trigger_screen_off_out", + "one_handed_settings_enabled_on", + "one_handed_settings_enabled_off", + "one_handed_settings_app_taps_exit_on", + "one_handed_settings_app_taps_exit_off", + "one_handed_settings_timeout_exit_on", + "one_handed_settings_timeout_exit_off", + "one_handed_settings_timeout_seconds_never", + "one_handed_settings_timeout_seconds_4", + "one_handed_settings_timeout_seconds_8", + "one_handed_settings_timeout_seconds_12" + }; + + /** + * Events definition that related to One-Handed gestures. + */ + @VisibleForTesting + public enum OneHandedTriggerEvent implements UiEventLogger.UiEventEnum { + INVALID(0), + @UiEvent(doc = "One-Handed trigger in via NavigationBar area") + ONE_HANDED_TRIGGER_GESTURE_IN(366), + + @UiEvent(doc = "One-Handed trigger out via NavigationBar area") + ONE_HANDED_TRIGGER_GESTURE_OUT(367), + + @UiEvent(doc = "One-Handed trigger out via Overspace area") + ONE_HANDED_TRIGGER_OVERSPACE_OUT(368), + + @UiEvent(doc = "One-Handed trigger out while IME pop up") + ONE_HANDED_TRIGGER_POP_IME_OUT(369), + + @UiEvent(doc = "One-Handed trigger out while device rotation to landscape") + ONE_HANDED_TRIGGER_ROTATION_OUT(370), + + @UiEvent(doc = "One-Handed trigger out when an Activity is launching") + ONE_HANDED_TRIGGER_APP_TAPS_OUT(371), + + @UiEvent(doc = "One-Handed trigger out when one-handed mode times up") + ONE_HANDED_TRIGGER_TIMEOUT_OUT(372), + + @UiEvent(doc = "One-Handed trigger out when screen off") + ONE_HANDED_TRIGGER_SCREEN_OFF_OUT(449); + + private final int mId; + + OneHandedTriggerEvent(int id) { + mId = id; + } + + public int getId() { + return mId; + } + } + + /** + * Events definition that related to Settings toggles. + */ + @VisibleForTesting + public enum OneHandedSettingsTogglesEvent implements UiEventLogger.UiEventEnum { + INVALID(0), + @UiEvent(doc = "One-Handed mode enabled toggle on") + ONE_HANDED_SETTINGS_TOGGLES_ENABLED_ON(356), + + @UiEvent(doc = "One-Handed mode enabled toggle off") + ONE_HANDED_SETTINGS_TOGGLES_ENABLED_OFF(357), + + @UiEvent(doc = "One-Handed mode app-taps-exit toggle on") + ONE_HANDED_SETTINGS_TOGGLES_APP_TAPS_EXIT_ON(358), + + @UiEvent(doc = "One-Handed mode app-taps-exit toggle off") + ONE_HANDED_SETTINGS_TOGGLES_APP_TAPS_EXIT_OFF(359), + + @UiEvent(doc = "One-Handed mode timeout-exit toggle on") + ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_EXIT_ON(360), + + @UiEvent(doc = "One-Handed mode timeout-exit toggle off") + ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_EXIT_OFF(361), + + @UiEvent(doc = "One-Handed mode timeout value changed to never timeout") + ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_NEVER(362), + + @UiEvent(doc = "One-Handed mode timeout value changed to 4 seconds") + ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_4(363), + + @UiEvent(doc = "One-Handed mode timeout value changed to 8 seconds") + ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_8(364), + + @UiEvent(doc = "One-Handed mode timeout value changed to 12 seconds") + ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_12(365); + + private final int mId; + + OneHandedSettingsTogglesEvent(int id) { + mId = id; + } + + public int getId() { + return mId; + } + } + + + /** + * Logs an event to the system log, to sCallback if present, and to the logEvent destinations. + * @param tag One of the EVENT_* codes above. + */ + public static void writeEvent(int tag) { + final long time = System.currentTimeMillis(); + logEvent(tag); + if (sCallback != null) { + sCallback.writeEvent(time, tag); + } + } + + /** + * Logs an event to the UiEvent (statsd) logging. + * @param event One of the EVENT_* codes above. + * @return String a readable description of the event. Begins "writeEvent <tag_description>" + * if the tag is valid. + */ + public static String logEvent(int event) { + if (event >= EVENT_TAGS.length) { + return ""; + } + final StringBuilder sb = new StringBuilder("writeEvent ").append(EVENT_TAGS[event]); + switch (event) { + // Triggers + case EVENT_ONE_HANDED_TRIGGER_GESTURE_IN: + sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_GESTURE_IN); + break; + case EVENT_ONE_HANDED_TRIGGER_GESTURE_OUT: + sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_GESTURE_OUT); + break; + case EVENT_ONE_HANDED_TRIGGER_OVERSPACE_OUT: + sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_OVERSPACE_OUT); + break; + case EVENT_ONE_HANDED_TRIGGER_POP_IME_OUT: + sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_POP_IME_OUT); + break; + case EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT: + sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_ROTATION_OUT); + break; + case EVENT_ONE_HANDED_TRIGGER_APP_TAPS_OUT: + sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_APP_TAPS_OUT); + break; + case EVENT_ONE_HANDED_TRIGGER_TIMEOUT_OUT: + sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_TIMEOUT_OUT); + break; + case EVENT_ONE_HANDED_TRIGGER_SCREEN_OFF_OUT: + sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_SCREEN_OFF_OUT); + break; + // Settings + case EVENT_ONE_HANDED_SETTINGS_ENABLED_ON: + sUiEventLogger.log(OneHandedSettingsTogglesEvent + .ONE_HANDED_SETTINGS_TOGGLES_ENABLED_ON); + break; + case EVENT_ONE_HANDED_SETTINGS_ENABLED_OFF: + sUiEventLogger.log(OneHandedSettingsTogglesEvent + .ONE_HANDED_SETTINGS_TOGGLES_ENABLED_OFF); + break; + case EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_ON: + sUiEventLogger.log(OneHandedSettingsTogglesEvent + .ONE_HANDED_SETTINGS_TOGGLES_APP_TAPS_EXIT_ON); + break; + case EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_OFF: + sUiEventLogger.log(OneHandedSettingsTogglesEvent + .ONE_HANDED_SETTINGS_TOGGLES_APP_TAPS_EXIT_OFF); + break; + case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_ON: + sUiEventLogger.log(OneHandedSettingsTogglesEvent + .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_EXIT_ON); + break; + case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_OFF: + sUiEventLogger.log(OneHandedSettingsTogglesEvent + .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_EXIT_OFF); + break; + case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_NEVER: + sUiEventLogger.log(OneHandedSettingsTogglesEvent + .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_NEVER); + break; + case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_4: + sUiEventLogger.log(OneHandedSettingsTogglesEvent + .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_4); + break; + case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_8: + sUiEventLogger.log(OneHandedSettingsTogglesEvent + .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_8); + break; + case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_12: + sUiEventLogger.log(OneHandedSettingsTogglesEvent + .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_12); + break; + default: + // Do nothing + break; + } + return sb.toString(); + } + + /** + * An interface for logging an event to the system log, if Callback present. + */ + public interface Callback { + /** + * + * @param time System current time. + * @param tag Event tag. + */ + void writeEvent(long time, int tag); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedGestureHandler.java new file mode 100644 index 000000000000..3b1e6cbe5ccd --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedGestureHandler.java @@ -0,0 +1,269 @@ +/* + * 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.onehanded; + +import static android.view.Display.DEFAULT_DISPLAY; + +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.hardware.input.InputManager; +import android.os.Looper; +import android.util.Log; +import android.view.Display; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.InputMonitor; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.ViewConfiguration; +import android.window.WindowContainerTransaction; + +import androidx.annotation.VisibleForTesting; + +import com.android.wm.shell.R; +import com.android.wm.shell.common.DisplayChangeController; +import com.android.wm.shell.common.DisplayController; + +/** + * The class manage swipe up and down gesture for 3-Button mode navigation, + * others(e.g, 2-button, full gesture mode) are handled by Launcher quick steps. + */ +public class OneHandedGestureHandler implements OneHandedTransitionCallback, + DisplayChangeController.OnDisplayChangingListener { + private static final String TAG = "OneHandedGestureHandler"; + private static final boolean DEBUG_GESTURE = false; + + private static final int ANGLE_MAX = 150; + private static final int ANGLE_MIN = 30; + private final float mDragDistThreshold; + private final float mSquaredSlop; + private final PointF mDownPos = new PointF(); + private final PointF mLastPos = new PointF(); + private final PointF mStartDragPos = new PointF(); + private boolean mPassedSlop; + + private boolean mAllowGesture; + private boolean mIsEnabled; + private int mNavGestureHeight; + private boolean mIsThreeButtonModeEnabled; + private int mRotation = Surface.ROTATION_0; + + @VisibleForTesting + InputMonitor mInputMonitor; + @VisibleForTesting + InputEventReceiver mInputEventReceiver; + private DisplayController mDisplayController; + @VisibleForTesting + @Nullable + OneHandedGestureEventCallback mGestureEventCallback; + private Rect mGestureRegion = new Rect(); + + /** + * Constructor of OneHandedGestureHandler, we only handle the gesture of + * {@link Display#DEFAULT_DISPLAY} + * + * @param context {@link Context} + * @param displayController {@link DisplayController} + */ + public OneHandedGestureHandler(Context context, DisplayController displayController) { + mDisplayController = displayController; + displayController.addDisplayChangingController(this); + mNavGestureHeight = context.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.navigation_bar_gesture_height); + mDragDistThreshold = context.getResources().getDimensionPixelSize( + R.dimen.gestures_onehanded_drag_threshold); + final float slop = ViewConfiguration.get(context).getScaledTouchSlop(); + mSquaredSlop = slop * slop; + updateIsEnabled(); + } + + /** + * Notified by {@link OneHandedController}, when user update settings of Enabled or Disabled + * + * @param isEnabled is one handed settings enabled or not + */ + public void onOneHandedEnabled(boolean isEnabled) { + if (DEBUG_GESTURE) { + Log.d(TAG, "onOneHandedEnabled, isEnabled = " + isEnabled); + } + mIsEnabled = isEnabled; + updateIsEnabled(); + } + + void onThreeButtonModeEnabled(boolean isEnabled) { + mIsThreeButtonModeEnabled = isEnabled; + updateIsEnabled(); + } + + /** + * Register {@link OneHandedGestureEventCallback} to receive onStart(), onStop() callback + */ + public void setGestureEventListener(OneHandedGestureEventCallback callback) { + mGestureEventCallback = callback; + } + + private void onMotionEvent(MotionEvent ev) { + int action = ev.getActionMasked(); + if (action == MotionEvent.ACTION_DOWN) { + mAllowGesture = isWithinTouchRegion(ev.getX(), ev.getY()) + && mRotation == Surface.ROTATION_0; + if (mAllowGesture) { + mDownPos.set(ev.getX(), ev.getY()); + mLastPos.set(mDownPos); + } + if (DEBUG_GESTURE) { + Log.d(TAG, "ACTION_DOWN, mDownPos=" + mDownPos + ", mAllowGesture=" + + mAllowGesture); + } + } else if (mAllowGesture) { + switch (action) { + case MotionEvent.ACTION_MOVE: + mLastPos.set(ev.getX(), ev.getY()); + if (!mPassedSlop) { + if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) + > mSquaredSlop) { + mStartDragPos.set(mLastPos.x, mLastPos.y); + if (isValidStartAngle( + mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y) + || isValidExitAngle( + mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y)) { + mPassedSlop = true; + mInputMonitor.pilferPointers(); + } + } + } else { + float distance = (float) Math.hypot(mLastPos.x - mDownPos.x, + mLastPos.y - mDownPos.y); + if (distance > mDragDistThreshold) { + mGestureEventCallback.onStop(); + } + } + break; + case MotionEvent.ACTION_UP: + if (mLastPos.y >= mDownPos.y && mPassedSlop) { + mGestureEventCallback.onStart(); + } + mPassedSlop = false; + mAllowGesture = false; + break; + case MotionEvent.ACTION_CANCEL: + mPassedSlop = false; + mAllowGesture = false; + break; + default: + break; + } + } + } + + private void disposeInputChannel() { + if (mInputEventReceiver != null) { + mInputEventReceiver.dispose(); + mInputEventReceiver = null; + } + + if (mInputMonitor != null) { + mInputMonitor.dispose(); + mInputMonitor = null; + } + } + + private boolean isWithinTouchRegion(float x, float y) { + if (DEBUG_GESTURE) { + Log.d(TAG, "isWithinTouchRegion(), mGestureRegion=" + mGestureRegion + ", downX=" + x + + ", downY=" + y); + } + return mGestureRegion.contains(Math.round(x), Math.round(y)); + } + + private void updateIsEnabled() { + disposeInputChannel(); + + if (mIsEnabled && mIsThreeButtonModeEnabled) { + final Point displaySize = new Point(); + if (mDisplayController != null) { + final Display display = mDisplayController.getDisplay(DEFAULT_DISPLAY); + if (display != null) { + display.getRealSize(displaySize); + } + } + // Register input event receiver to monitor the touch region of NavBar gesture height + mGestureRegion.set(0, displaySize.y - mNavGestureHeight, displaySize.x, + displaySize.y); + mInputMonitor = InputManager.getInstance().monitorGestureInput( + "onehanded-gesture-offset", DEFAULT_DISPLAY); + mInputEventReceiver = new EventReceiver( + mInputMonitor.getInputChannel(), Looper.getMainLooper()); + } + } + + private void onInputEvent(InputEvent ev) { + if (ev instanceof MotionEvent) { + onMotionEvent((MotionEvent) ev); + } + } + + @Override + public void onRotateDisplay(int displayId, int fromRotation, int toRotation, + WindowContainerTransaction t) { + mRotation = toRotation; + } + + private class EventReceiver extends InputEventReceiver { + EventReceiver(InputChannel channel, Looper looper) { + super(channel, looper); + } + + public void onInputEvent(InputEvent event) { + OneHandedGestureHandler.this.onInputEvent(event); + finishInputEvent(event, true); + } + } + + private boolean isValidStartAngle(float deltaX, float deltaY) { + final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX)); + return angle > -(ANGLE_MAX) && angle < -(ANGLE_MIN); + } + + private boolean isValidExitAngle(float deltaX, float deltaY) { + final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX)); + return angle > ANGLE_MIN && angle < ANGLE_MAX; + } + + private float squaredHypot(float x, float y) { + return x * x + y * y; + } + + /** + * The touch(gesture) events to notify {@link OneHandedController} start or stop one handed + */ + public interface OneHandedGestureEventCallback { + /** + * Handles the start gesture. + */ + void onStart(); + + /** + * Handles the exit gesture. + */ + void onStop(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java new file mode 100644 index 000000000000..4d66f2961a29 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java @@ -0,0 +1,144 @@ +/* + * 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.onehanded; + +import android.annotation.IntDef; +import android.content.ContentResolver; +import android.database.ContentObserver; +import android.net.Uri; +import android.provider.Settings; + +import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * APIs for querying or updating one handed settings . + */ +public final class OneHandedSettingsUtil { + private static final String TAG = "OneHandedSettingsUtil"; + + @IntDef(prefix = {"ONE_HANDED_TIMEOUT_"}, value = { + ONE_HANDED_TIMEOUT_NEVER, + ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS, + ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS, + ONE_HANDED_TIMEOUT_LONG_IN_SECONDS, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface OneHandedTimeout { + } + + /** + * Never stop one handed automatically + */ + public static final int ONE_HANDED_TIMEOUT_NEVER = 0; + /** + * Auto stop one handed in {@link OneHandedSettingsUtil#ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS} + */ + public static final int ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS = 4; + /** + * Auto stop one handed in {@link OneHandedSettingsUtil#ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS} + */ + public static final int ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS = 8; + /** + * Auto stop one handed in {@link OneHandedSettingsUtil#ONE_HANDED_TIMEOUT_LONG_IN_SECONDS} + */ + public static final int ONE_HANDED_TIMEOUT_LONG_IN_SECONDS = 12; + + /** + * Register one handed preference settings observer + * + * @param key Setting key to monitor in observer + * @param resolver ContentResolver of context + * @param observer Observer from caller + * @return uri key for observing + */ + public static Uri registerSettingsKeyObserver(String key, ContentResolver resolver, + ContentObserver observer) { + Uri uriKey = null; + uriKey = Settings.Secure.getUriFor(key); + if (resolver != null && uriKey != null) { + resolver.registerContentObserver(uriKey, false, observer); + } + return uriKey; + } + + /** + * Unregister one handed preference settings observer + * + * @param resolver ContentResolver of context + * @param observer preference key change observer + */ + public static void unregisterSettingsKeyObserver(ContentResolver resolver, + ContentObserver observer) { + if (resolver != null) { + resolver.unregisterContentObserver(observer); + } + } + + /** + * Query one handed enable or disable flag from Settings provider. + * + * @return enable or disable one handed mode flag. + */ + public static boolean getSettingsOneHandedModeEnabled(ContentResolver resolver) { + return Settings.Secure.getInt(resolver, + Settings.Secure.ONE_HANDED_MODE_ENABLED, 0 /* Disabled */) == 1; + } + + /** + * Query taps app to exit config from Settings provider. + * + * @return enable or disable taps app exit. + */ + public static boolean getSettingsTapsAppToExit(ContentResolver resolver) { + return Settings.Secure.getInt(resolver, + Settings.Secure.TAPS_APP_TO_EXIT, 0) == 1; + } + + /** + * Query timeout value from Settings provider. + * Default is {@link OneHandedSettingsUtil#ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS} + * + * @return timeout value in seconds. + */ + public static @OneHandedTimeout int getSettingsOneHandedModeTimeout(ContentResolver resolver) { + return Settings.Secure.getInt(resolver, + Settings.Secure.ONE_HANDED_MODE_TIMEOUT, ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS); + } + + /** + * Returns whether swipe bottom to notification gesture enabled or not. + */ + public static boolean getSettingsSwipeToNotificationEnabled(ContentResolver resolver) { + return Settings.Secure.getInt(resolver, + Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED, 0) == 1; + } + + protected static void dump(PrintWriter pw, String prefix, ContentResolver resolver) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.print(innerPrefix + "isOneHandedModeEnable="); + pw.println(getSettingsOneHandedModeEnabled(resolver)); + pw.print(innerPrefix + "oneHandedTimeOut="); + pw.println(getSettingsOneHandedModeTimeout(resolver)); + pw.print(innerPrefix + "tapsAppToExit="); + pw.println(getSettingsTapsAppToExit(resolver)); + } + + private OneHandedSettingsUtil() {} +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSurfaceTransactionHelper.java new file mode 100644 index 000000000000..e7010db97d77 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSurfaceTransactionHelper.java @@ -0,0 +1,88 @@ +/* + * 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.onehanded; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.view.SurfaceControl; + +import com.android.wm.shell.R; + +/** + * Abstracts the common operations on {@link SurfaceControl.Transaction} for OneHanded transition. + */ +public class OneHandedSurfaceTransactionHelper { + private final boolean mEnableCornerRadius; + private final float mCornerRadius; + + public OneHandedSurfaceTransactionHelper(Context context) { + final Resources res = context.getResources(); + mCornerRadius = res.getDimension(com.android.internal.R.dimen.rounded_corner_radius); + mEnableCornerRadius = res.getBoolean(R.bool.config_one_handed_enable_round_corner); + } + + /** + * Operates the translation (setPosition) on a given transaction and leash + * + * @return same {@link OneHandedSurfaceTransactionHelper} instance for method chaining + */ + OneHandedSurfaceTransactionHelper translate(SurfaceControl.Transaction tx, SurfaceControl leash, + float offset) { + tx.setPosition(leash, 0, offset); + return this; + } + + /** + * Operates the alpha on a given transaction and leash + * + * @return same {@link OneHandedSurfaceTransactionHelper} instance for method chaining + */ + OneHandedSurfaceTransactionHelper alpha(SurfaceControl.Transaction tx, SurfaceControl leash, + float alpha) { + tx.setAlpha(leash, alpha); + return this; + } + + /** + * Operates the crop (setMatrix) on a given transaction and leash + * + * @return same {@link OneHandedSurfaceTransactionHelper} instance for method chaining + */ + OneHandedSurfaceTransactionHelper crop(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect destinationBounds) { + tx.setWindowCrop(leash, destinationBounds.width(), destinationBounds.height()) + .setPosition(leash, destinationBounds.left, destinationBounds.top); + return this; + } + + /** + * Operates the round corner radius on a given transaction and leash + * + * @return same {@link OneHandedSurfaceTransactionHelper} instance for method chaining + */ + OneHandedSurfaceTransactionHelper round(SurfaceControl.Transaction tx, SurfaceControl leash) { + if (mEnableCornerRadius) { + tx.setCornerRadius(leash, mCornerRadius); + } + return this; + } + + interface SurfaceControlTransactionFactory { + SurfaceControl.Transaction getTransaction(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedThread.java new file mode 100644 index 000000000000..24d33ede5d63 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedThread.java @@ -0,0 +1,62 @@ +/* + * 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.onehanded; + +import android.os.Handler; +import android.os.HandlerThread; + +/** + * Similar to {@link com.android.internal.os.BackgroundThread}, this is a shared singleton + * foreground thread for each process for updating one handed. + */ +public class OneHandedThread extends HandlerThread { + private static OneHandedThread sInstance; + private static Handler sHandler; + + private OneHandedThread() { + super("OneHanded"); + } + + private static void ensureThreadLocked() { + if (sInstance == null) { + sInstance = new OneHandedThread(); + sInstance.start(); + sHandler = new Handler(sInstance.getLooper()); + } + } + + /** + * @return the static update thread instance + */ + public static OneHandedThread get() { + synchronized (OneHandedThread.class) { + ensureThreadLocked(); + return sInstance; + } + } + + /** + * @return the static update thread handler instance + */ + public static Handler getHandler() { + synchronized (OneHandedThread.class) { + ensureThreadLocked(); + return sHandler; + } + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandler.java new file mode 100644 index 000000000000..9c97cd7db71f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandler.java @@ -0,0 +1,159 @@ +/* + * 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.onehanded; + +import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Timeout handler for stop one handed mode operations. + */ +public class OneHandedTimeoutHandler { + private static final String TAG = "OneHandedTimeoutHandler"; + private static boolean sIsDragging = false; + // Default timeout is ONE_HANDED_TIMEOUT_MEDIUM + private static @OneHandedSettingsUtil.OneHandedTimeout int sTimeout = + ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS; + private static long sTimeoutMs = TimeUnit.SECONDS.toMillis(sTimeout); + private static OneHandedTimeoutHandler sInstance; + private static List<TimeoutListener> sListeners = new ArrayList<>(); + + @VisibleForTesting + static final int ONE_HANDED_TIMEOUT_STOP_MSG = 1; + @VisibleForTesting + static Handler sHandler; + + /** + * Get the current config of timeout + * + * @return timeout of current config + */ + public @OneHandedSettingsUtil.OneHandedTimeout int getTimeout() { + return sTimeout; + } + + /** + * Listens for notify timeout events + */ + public interface TimeoutListener { + /** + * Called whenever the config time out + * + * @param timeoutTime The time in seconds to trigger timeout + */ + void onTimeout(int timeoutTime); + } + + /** + * Set the specific timeout of {@link OneHandedSettingsUtil.OneHandedTimeout} + */ + public static void setTimeout(@OneHandedSettingsUtil.OneHandedTimeout int timeout) { + sTimeout = timeout; + sTimeoutMs = TimeUnit.SECONDS.toMillis(sTimeout); + resetTimer(); + } + + /** + * Reset the timer when one handed trigger or user is operating in some conditions + */ + public static void removeTimer() { + sHandler.removeMessages(ONE_HANDED_TIMEOUT_STOP_MSG); + } + + /** + * Reset the timer when one handed trigger or user is operating in some conditions + */ + public static void resetTimer() { + removeTimer(); + if (sTimeout == OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER) { + return; + } + if (sTimeout != OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER) { + sHandler.sendEmptyMessageDelayed(ONE_HANDED_TIMEOUT_STOP_MSG, sTimeoutMs); + } + } + + /** + * Register timeout listener to receive time out events + * + * @param listener the listener be sent events when times up + */ + public static void registerTimeoutListener(TimeoutListener listener) { + sListeners.add(listener); + } + + /** + * Private constructor due to Singleton pattern + */ + private OneHandedTimeoutHandler() { + } + + /** + * Singleton pattern to get {@link OneHandedTimeoutHandler} instance + * + * @return the static update thread instance + */ + public static OneHandedTimeoutHandler get() { + synchronized (OneHandedTimeoutHandler.class) { + if (sInstance == null) { + sInstance = new OneHandedTimeoutHandler(); + } + if (sHandler == null) { + sHandler = new Handler(Looper.myLooper()) { + @Override + public void handleMessage(Message msg) { + if (msg.what == ONE_HANDED_TIMEOUT_STOP_MSG) { + onStop(); + } + } + }; + if (sTimeout != OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER) { + sHandler.sendEmptyMessageDelayed(ONE_HANDED_TIMEOUT_STOP_MSG, sTimeoutMs); + } + } + return sInstance; + } + } + + private static void onStop() { + for (int i = sListeners.size() - 1; i >= 0; i--) { + final TimeoutListener listener = sListeners.get(i); + listener.onTimeout(sTimeout); + } + } + + void dump(@NonNull PrintWriter pw) { + final String innerPrefix = " "; + pw.println(TAG + "states: "); + pw.print(innerPrefix + "sTimeout="); + pw.println(sTimeout); + pw.print(innerPrefix + "sListeners="); + pw.println(sListeners); + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTouchHandler.java new file mode 100644 index 000000000000..721382d52717 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTouchHandler.java @@ -0,0 +1,173 @@ +/* + * 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.onehanded; + +import static android.view.Display.DEFAULT_DISPLAY; + +import android.graphics.Rect; +import android.hardware.input.InputManager; +import android.os.Looper; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.InputMonitor; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import java.io.PrintWriter; + +/** + * Manages all the touch handling for One Handed on the Phone, including user tap outside region + * to exit, reset timer when user is in one-handed mode. + * Refer {@link OneHandedGestureHandler} to see start and stop one handed gesture + */ +public class OneHandedTouchHandler implements OneHandedTransitionCallback { + private static final String TAG = "OneHandedTouchHandler"; + private final Rect mLastUpdatedBounds = new Rect(); + + private OneHandedTimeoutHandler mTimeoutHandler; + + @VisibleForTesting + InputMonitor mInputMonitor; + @VisibleForTesting + InputEventReceiver mInputEventReceiver; + @VisibleForTesting + OneHandedTouchEventCallback mTouchEventCallback; + + private boolean mIsEnabled; + private boolean mIsOnStopTransitioning; + private boolean mIsInOutsideRegion; + + public OneHandedTouchHandler() { + mTimeoutHandler = OneHandedTimeoutHandler.get(); + updateIsEnabled(); + } + + /** + * Notified by {@link OneHandedController}, when user update settings of Enabled or Disabled + * + * @param isEnabled is one handed settings enabled or not + */ + public void onOneHandedEnabled(boolean isEnabled) { + mIsEnabled = isEnabled; + updateIsEnabled(); + } + + /** + * Register {@link OneHandedTouchEventCallback} to receive onEnter(), onExit() callback + */ + public void registerTouchEventListener(OneHandedTouchEventCallback callback) { + mTouchEventCallback = callback; + } + + private boolean onMotionEvent(MotionEvent ev) { + mIsInOutsideRegion = isWithinTouchOutsideRegion(ev.getX(), ev.getY()); + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: { + if (!mIsInOutsideRegion) { + mTimeoutHandler.resetTimer(); + } + break; + } + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + mTimeoutHandler.resetTimer(); + if (mIsInOutsideRegion && !mIsOnStopTransitioning) { + mTouchEventCallback.onStop(); + mIsOnStopTransitioning = true; + } + // Reset flag for next operation + mIsInOutsideRegion = false; + break; + } + } + return true; + } + + private void disposeInputChannel() { + if (mInputEventReceiver != null) { + mInputEventReceiver.dispose(); + mInputEventReceiver = null; + } + if (mInputMonitor != null) { + mInputMonitor.dispose(); + mInputMonitor = null; + } + } + + private boolean isWithinTouchOutsideRegion(float x, float y) { + return Math.round(y) < mLastUpdatedBounds.top; + } + + private void onInputEvent(InputEvent ev) { + if (ev instanceof MotionEvent) { + onMotionEvent((MotionEvent) ev); + } + } + + private void updateIsEnabled() { + disposeInputChannel(); + if (mIsEnabled) { + mInputMonitor = InputManager.getInstance().monitorGestureInput( + "onehanded-touch", DEFAULT_DISPLAY); + mInputEventReceiver = new EventReceiver( + mInputMonitor.getInputChannel(), Looper.getMainLooper()); + } + } + + @Override + public void onStartFinished(Rect bounds) { + mLastUpdatedBounds.set(bounds); + } + + @Override + public void onStopFinished(Rect bounds) { + mLastUpdatedBounds.set(bounds); + mIsOnStopTransitioning = false; + } + + void dump(@NonNull PrintWriter pw) { + final String innerPrefix = " "; + pw.println(TAG + "states: "); + pw.print(innerPrefix + "mLastUpdatedBounds="); + pw.println(mLastUpdatedBounds); + } + + private class EventReceiver extends InputEventReceiver { + EventReceiver(InputChannel channel, Looper looper) { + super(channel, looper); + } + + public void onInputEvent(InputEvent event) { + OneHandedTouchHandler.this.onInputEvent(event); + finishInputEvent(event, true); + } + } + + /** + * The touch(gesture) events to notify {@link OneHandedController} start or stop one handed + */ + public interface OneHandedTouchEventCallback { + /** + * Handle the exit event. + */ + void onStop(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTransitionCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTransitionCallback.java new file mode 100644 index 000000000000..3af7c4b71d0a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTransitionCallback.java @@ -0,0 +1,37 @@ +/* + * 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.onehanded; + +import android.graphics.Rect; + +/** + * The start or stop one handed transition callback for gesture to get latest timing to handle + * touch region.(e.g: one handed activated, user tap out regions of displayArea to stop one handed) + */ +public interface OneHandedTransitionCallback { + /** + * Called when start one handed transition finished + */ + default void onStartFinished(Rect bounds) { + } + + /** + * Called when stop one handed transition finished + */ + default void onStopFinished(Rect bounds) { + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTutorialHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTutorialHandler.java new file mode 100644 index 000000000000..b6b518d69c55 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTutorialHandler.java @@ -0,0 +1,221 @@ +/* + * 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.onehanded; + +import android.content.ContentResolver; +import android.content.Context; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Handler; +import android.os.SystemProperties; +import android.provider.Settings; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; + +import com.android.wm.shell.R; + +import java.io.PrintWriter; + +/** + * Manages the user tutorial handling for One Handed operations, including animations synchronized + * with one-handed translation. + * Refer {@link OneHandedGestureHandler} and {@link OneHandedTouchHandler} to see start and stop + * one handed gesture + */ +public class OneHandedTutorialHandler implements OneHandedTransitionCallback { + private static final String TAG = "OneHandedTutorialHandler"; + private static final String ONE_HANDED_MODE_OFFSET_PERCENTAGE = + "persist.debug.one_handed_offset_percentage"; + private static final int MAX_TUTORIAL_SHOW_COUNT = 2; + private final Rect mLastUpdatedBounds = new Rect(); + private final WindowManager mWindowManager; + private final AccessibilityManager mAccessibilityManager; + private final String mPackageName; + + private View mTutorialView; + private Point mDisplaySize = new Point(); + private Handler mUpdateHandler; + private ContentResolver mContentResolver; + private boolean mCanShowTutorial; + private String mStartOneHandedDescription; + private String mStopOneHandedDescription; + + /** + * Container of the tutorial panel showing at outside region when one handed starting + */ + private ViewGroup mTargetViewContainer; + private int mTutorialAreaHeight; + + private final OneHandedAnimationCallback mAnimationCallback = new OneHandedAnimationCallback() { + @Override + public void onTutorialAnimationUpdate(int offset) { + mUpdateHandler.post(() -> onAnimationUpdate(offset)); + } + }; + + public OneHandedTutorialHandler(Context context) { + context.getDisplay().getRealSize(mDisplaySize); + mPackageName = context.getPackageName(); + mContentResolver = context.getContentResolver(); + mUpdateHandler = new Handler(); + mWindowManager = context.getSystemService(WindowManager.class); + mAccessibilityManager = (AccessibilityManager) + context.getSystemService(Context.ACCESSIBILITY_SERVICE); + mTargetViewContainer = new FrameLayout(context); + mTargetViewContainer.setClipChildren(false); + mTutorialAreaHeight = Math.round(mDisplaySize.y + * (SystemProperties.getInt(ONE_HANDED_MODE_OFFSET_PERCENTAGE, 50) / 100.0f)); + mTutorialView = LayoutInflater.from(context).inflate(R.layout.one_handed_tutorial, null); + mTargetViewContainer.addView(mTutorialView); + mCanShowTutorial = (Settings.Secure.getInt(mContentResolver, + Settings.Secure.ONE_HANDED_TUTORIAL_SHOW_COUNT, 0) >= MAX_TUTORIAL_SHOW_COUNT) + ? false : true; + mStartOneHandedDescription = context.getResources().getString( + R.string.accessibility_action_start_one_handed); + mStopOneHandedDescription = context.getResources().getString( + R.string.accessibility_action_stop_one_handed); + if (mCanShowTutorial) { + createOrUpdateTutorialTarget(); + } + } + + @Override + public void onStartFinished(Rect bounds) { + mUpdateHandler.post(() -> { + updateFinished(View.VISIBLE, 0f); + updateTutorialCount(); + announcementForScreenReader(true); + }); + } + + @Override + public void onStopFinished(Rect bounds) { + mUpdateHandler.post(() -> { + updateFinished(View.INVISIBLE, -mTargetViewContainer.getHeight()); + announcementForScreenReader(false); + }); + } + + private void updateFinished(int visible, float finalPosition) { + if (!canShowTutorial()) { + return; + } + + mTargetViewContainer.setVisibility(visible); + mTargetViewContainer.setTranslationY(finalPosition); + } + + private void updateTutorialCount() { + int showCount = Settings.Secure.getInt(mContentResolver, + Settings.Secure.ONE_HANDED_TUTORIAL_SHOW_COUNT, 0); + showCount = Math.min(MAX_TUTORIAL_SHOW_COUNT, showCount + 1); + mCanShowTutorial = showCount < MAX_TUTORIAL_SHOW_COUNT; + Settings.Secure.putInt(mContentResolver, + Settings.Secure.ONE_HANDED_TUTORIAL_SHOW_COUNT, showCount); + } + + private void announcementForScreenReader(boolean isStartOneHanded) { + if (mAccessibilityManager.isTouchExplorationEnabled()) { + final AccessibilityEvent event = AccessibilityEvent.obtain(); + event.setPackageName(mPackageName); + event.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT); + event.getText().add(isStartOneHanded + ? mStartOneHandedDescription : mStopOneHandedDescription); + mAccessibilityManager.sendAccessibilityEvent(event); + } + } + + /** + * Adds the tutorial target view to the WindowManager and update its layout, so it's ready + * to be animated in. + */ + private void createOrUpdateTutorialTarget() { + mUpdateHandler.post(() -> { + if (!mTargetViewContainer.isAttachedToWindow()) { + mTargetViewContainer.setVisibility(View.INVISIBLE); + + try { + mWindowManager.addView(mTargetViewContainer, getTutorialTargetLayoutParams()); + } catch (IllegalStateException e) { + // This shouldn't happen, but if the target is already added, just update its + // layout params. + mWindowManager.updateViewLayout( + mTargetViewContainer, getTutorialTargetLayoutParams()); + } + } else { + mWindowManager.updateViewLayout(mTargetViewContainer, + getTutorialTargetLayoutParams()); + } + }); + } + + OneHandedAnimationCallback getAnimationCallback() { + return mAnimationCallback; + } + + /** + * Returns layout params for the dismiss target, using the latest display metrics. + */ + private WindowManager.LayoutParams getTutorialTargetLayoutParams() { + final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + mDisplaySize.x, mTutorialAreaHeight, 0, 0, + WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT); + lp.gravity = Gravity.TOP | Gravity.LEFT; + lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + lp.setFitInsetsTypes(0 /* types */); + lp.setTitle("one-handed-tutorial-overlay"); + + return lp; + } + + void dump(@NonNull PrintWriter pw) { + final String innerPrefix = " "; + pw.println(TAG + "states: "); + pw.print(innerPrefix + "mLastUpdatedBounds="); + pw.println(mLastUpdatedBounds); + } + + private boolean canShowTutorial() { + if (!mCanShowTutorial) { + mTargetViewContainer.setVisibility(View.GONE); + return false; + } + + return true; + } + + private void onAnimationUpdate(float value) { + if (!canShowTutorial()) { + return; + } + mTargetViewContainer.setVisibility(View.VISIBLE); + mTargetViewContainer.setTransitionGroup(true); + mTargetViewContainer.setTranslationY(value - mTargetViewContainer.getHeight()); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java new file mode 100644 index 000000000000..993e0e7ed016 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2019 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.pip; + +import android.app.RemoteAction; +import android.content.ComponentName; +import android.content.pm.ParceledListSlice; +import android.view.DisplayInfo; +import android.view.IPinnedStackController; +import android.view.IPinnedStackListener; + +import java.util.ArrayList; +import java.util.List; + +/** + * PinnedStackListener that simply forwards all calls to each listener added via + * {@link #addListener}. This is necessary since calling + * {@link com.android.server.wm.WindowManagerService#registerPinnedStackListener} replaces any + * previously set listener. + */ +public class PinnedStackListenerForwarder extends IPinnedStackListener.Stub { + private List<PinnedStackListener> mListeners = new ArrayList<>(); + + /** Adds a listener to receive updates from the WindowManagerService. */ + public void addListener(PinnedStackListener listener) { + mListeners.add(listener); + } + + /** Removes a listener so it will no longer receive updates from the WindowManagerService. */ + public void removeListener(PinnedStackListener listener) { + mListeners.remove(listener); + } + + @Override + public void onListenerRegistered(IPinnedStackController controller) { + for (PinnedStackListener listener : mListeners) { + listener.onListenerRegistered(controller); + } + } + + @Override + public void onMovementBoundsChanged(boolean fromImeAdjustment) { + for (PinnedStackListener listener : mListeners) { + listener.onMovementBoundsChanged(fromImeAdjustment); + } + } + + @Override + public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { + for (PinnedStackListener listener : mListeners) { + listener.onImeVisibilityChanged(imeVisible, imeHeight); + } + } + + @Override + public void onActionsChanged(ParceledListSlice<RemoteAction> actions) { + for (PinnedStackListener listener : mListeners) { + listener.onActionsChanged(actions); + } + } + + @Override + public void onActivityHidden(ComponentName componentName) { + for (PinnedStackListener listener : mListeners) { + listener.onActivityHidden(componentName); + } + } + + @Override + public void onDisplayInfoChanged(DisplayInfo displayInfo) { + for (PinnedStackListener listener : mListeners) { + listener.onDisplayInfoChanged(displayInfo); + } + } + + @Override + public void onConfigurationChanged() { + for (PinnedStackListener listener : mListeners) { + listener.onConfigurationChanged(); + } + } + + @Override + public void onAspectRatioChanged(float aspectRatio) { + for (PinnedStackListener listener : mListeners) { + listener.onAspectRatioChanged(aspectRatio); + } + } + + /** + * A counterpart of {@link IPinnedStackListener} with empty implementations. + * Subclasses can ignore those methods they do not intend to take action upon. + */ + public static class PinnedStackListener { + public void onListenerRegistered(IPinnedStackController controller) {} + + public void onMovementBoundsChanged(boolean fromImeAdjustment) {} + + public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {} + + public void onActionsChanged(ParceledListSlice<RemoteAction> actions) {} + + public void onActivityHidden(ComponentName componentName) {} + + public void onDisplayInfoChanged(DisplayInfo displayInfo) {} + + public void onConfigurationChanged() {} + + public void onAspectRatioChanged(float aspectRatio) {} + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java new file mode 100644 index 000000000000..3ded4091ec11 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java @@ -0,0 +1,262 @@ +/* + * 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.pip; + +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.PictureInPictureParams; +import android.content.ComponentName; +import android.content.pm.ActivityInfo; +import android.graphics.Rect; +import android.media.session.MediaController; + +import com.android.wm.shell.pip.phone.PipTouchHandler; +import com.android.wm.shell.pip.tv.PipController; + +import java.io.PrintWriter; +import java.util.function.Consumer; + +/** + * Interface to engage picture in picture feature. + */ +public interface Pip { + /** + * Registers {@link com.android.wm.shell.pip.tv.PipController.Listener} that gets called. + * whenever receiving notification on changes in PIP. + */ + default void addListener(PipController.Listener listener) { + } + + /** + * Registers a {@link PipController.MediaListener} to PipController. + */ + default void addMediaListener(PipController.MediaListener listener) { + } + + /** + * Closes PIP (PIPed activity and PIP system UI). + */ + default void closePip() { + } + + /** + * Dump the current state and information if need. + * + * @param pw The stream to dump information to. + */ + default void dump(PrintWriter pw) { + } + + /** + * Expand PIP, it's possible that specific request to activate the window via Alt-tab. + */ + default void expandPip() { + } + + /** + * Get current play back state. (e.g: Used in TV) + * + * @return The state of defined in PipController. + */ + default int getPlaybackState() { + return -1; + } + + /** + * Get the touch handler which manages all the touch handling for PIP on the Phone, + * including moving, dismissing and expanding the PIP. (Do not used in TV) + * + * @return + */ + default @Nullable PipTouchHandler getPipTouchHandler() { + return null; + } + + /** + * Get MediaController. + * + * @return The MediaController instance. + */ + default MediaController getMediaController() { + return null; + } + + /** + * Hides the PIP menu. + */ + default void hidePipMenu(Runnable onStartCallback, Runnable onEndCallback) {} + + /** + * Returns {@code true} if PIP is shown. + */ + default boolean isPipShown() { + return false; + } + + /** + * Moves the PIPed activity to the fullscreen and closes PIP system UI. + */ + default void movePipToFullscreen() { + } + + /** + * Called whenever an Activity is moved to the pinned stack from another stack. + */ + default void onActivityPinned(String packageName) { + } + + /** + * Called whenever an Activity is moved from the pinned stack to another stack + */ + default void onActivityUnpinned(ComponentName topActivity) { + } + + /** + * Called whenever IActivityManager.startActivity is called on an activity that is already + * running, but the task is either brought to the front or a new Intent is delivered to it. + * + * @param task information about the task the activity was relaunched into + * @param clearedTask whether or not the launch activity also cleared the task as a part of + * starting + */ + default void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, + boolean clearedTask) { + } + + /** + * Called when display size or font size of settings changed + */ + default void onDensityOrFontScaleChanged() { + } + + /** + * Called when overlay package change invoked. + */ + default void onOverlayChanged() { + } + + /** + * Registers the session listener for the current user. + */ + default void registerSessionListenerForCurrentUser() { + } + + /** + * Called when SysUI state changed. + * + * @param isSysUiStateValid Is SysUI state valid or not. + * @param flag Current SysUI state. + */ + default void onSystemUiStateChanged(boolean isSysUiStateValid, int flag) { + } + + /** + * Called when task stack changed. + */ + default void onTaskStackChanged() { + } + + /** + * Removes a {@link PipController.Listener} from PipController. + */ + default void removeListener(PipController.Listener listener) { + } + + /** + * Removes a {@link PipController.MediaListener} from PipController. + */ + default void removeMediaListener(PipController.MediaListener listener) { + } + + /** + * Resize the Pip to the appropriate size for the input state. + * + * @param state In Pip state also used to determine the new size for the Pip. + */ + default void resizePinnedStack(int state) { + } + + /** + * Resumes resizing operation on the Pip that was previously suspended. + * + * @param reason The reason resizing operations on the Pip was suspended. + */ + default void resumePipResizing(int reason) { + } + + /** + * Sets both shelf visibility and its height. + * + * @param visible visibility of shelf. + * @param height to specify the height for shelf. + */ + default void setShelfHeight(boolean visible, int height) { + } + + /** + * Registers the pinned stack animation listener. + * + * @param callback The callback of pinned stack animation. + */ + default void setPinnedStackAnimationListener(Consumer<Boolean> callback) { + } + + /** + * Set the pinned stack with {@link PipAnimationController.AnimationType} + * + * @param animationType The pre-defined {@link PipAnimationController.AnimationType} + */ + default void setPinnedStackAnimationType(int animationType) { + } + + /** + * Called when showing Pip menu. + */ + default void showPictureInPictureMenu() {} + + /** + * Suspends resizing operation on the Pip until {@link #resumePipResizing} is called. + * + * @param reason The reason for suspending resizing operations on the Pip. + */ + default void suspendPipResizing(int reason) { + } + + /** + * Called by Launcher when swiping an auto-pip enabled Activity to home starts + * @param componentName {@link ComponentName} represents the Activity entering PiP + * @param activityInfo {@link ActivityInfo} tied to the Activity + * @param pictureInPictureParams {@link PictureInPictureParams} tied to the Activity + * @param launcherRotation Rotation Launcher is in + * @param shelfHeight Shelf height when landing PiP window onto Launcher + * @return Destination bounds of PiP window based on the parameters passed in + */ + default Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, + PictureInPictureParams pictureInPictureParams, + int launcherRotation, int shelfHeight) { + return null; + } + + /** + * Called by Launcher when swiping an auto-pip enable Activity to home finishes + * @param componentName {@link ComponentName} represents the Activity entering PiP + * @param destinationBounds Destination bounds of PiP window + */ + default void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds) { + return; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java new file mode 100644 index 000000000000..d82946269ee8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -0,0 +1,462 @@ +/* + * 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.pip; + +import android.animation.AnimationHandler; +import android.animation.Animator; +import android.animation.RectEvaluator; +import android.animation.ValueAnimator; +import android.annotation.IntDef; +import android.graphics.Rect; +import android.view.SurfaceControl; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.graphics.SfVsyncFrameCallbackProvider; +import com.android.wm.shell.animation.Interpolators; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Controller class of PiP animations (both from and to PiP mode). + */ +public class PipAnimationController { + private static final float FRACTION_START = 0f; + private static final float FRACTION_END = 1f; + + public static final int ANIM_TYPE_BOUNDS = 0; + public static final int ANIM_TYPE_ALPHA = 1; + + @IntDef(prefix = { "ANIM_TYPE_" }, value = { + ANIM_TYPE_BOUNDS, + ANIM_TYPE_ALPHA + }) + @Retention(RetentionPolicy.SOURCE) + public @interface AnimationType {} + + public static final int TRANSITION_DIRECTION_NONE = 0; + public static final int TRANSITION_DIRECTION_SAME = 1; + public static final int TRANSITION_DIRECTION_TO_PIP = 2; + public static final int TRANSITION_DIRECTION_LEAVE_PIP = 3; + public static final int TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN = 4; + public static final int TRANSITION_DIRECTION_REMOVE_STACK = 5; + + @IntDef(prefix = { "TRANSITION_DIRECTION_" }, value = { + TRANSITION_DIRECTION_NONE, + TRANSITION_DIRECTION_SAME, + TRANSITION_DIRECTION_TO_PIP, + TRANSITION_DIRECTION_LEAVE_PIP, + TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN, + TRANSITION_DIRECTION_REMOVE_STACK + }) + @Retention(RetentionPolicy.SOURCE) + public @interface TransitionDirection {} + + public static boolean isInPipDirection(@TransitionDirection int direction) { + return direction == TRANSITION_DIRECTION_TO_PIP; + } + + public static boolean isOutPipDirection(@TransitionDirection int direction) { + return direction == TRANSITION_DIRECTION_LEAVE_PIP + || direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN; + } + + private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; + + private PipTransitionAnimator mCurrentAnimator; + + private ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal = + ThreadLocal.withInitial(() -> { + AnimationHandler handler = new AnimationHandler(); + handler.setProvider(new SfVsyncFrameCallbackProvider()); + return handler; + }); + + public PipAnimationController(PipSurfaceTransactionHelper helper) { + mSurfaceTransactionHelper = helper; + } + + @SuppressWarnings("unchecked") + @VisibleForTesting + public PipTransitionAnimator getAnimator(SurfaceControl leash, + Rect destinationBounds, float alphaStart, float alphaEnd) { + if (mCurrentAnimator == null) { + mCurrentAnimator = setupPipTransitionAnimator( + PipTransitionAnimator.ofAlpha(leash, destinationBounds, alphaStart, alphaEnd)); + } else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_ALPHA + && mCurrentAnimator.isRunning()) { + mCurrentAnimator.updateEndValue(alphaEnd); + } else { + mCurrentAnimator.cancel(); + mCurrentAnimator = setupPipTransitionAnimator( + PipTransitionAnimator.ofAlpha(leash, destinationBounds, alphaStart, alphaEnd)); + } + return mCurrentAnimator; + } + + @SuppressWarnings("unchecked") + @VisibleForTesting + public PipTransitionAnimator getAnimator(SurfaceControl leash, Rect startBounds, Rect endBounds, + Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction) { + if (mCurrentAnimator == null) { + mCurrentAnimator = setupPipTransitionAnimator( + PipTransitionAnimator.ofBounds(leash, startBounds, endBounds, sourceHintRect, + direction)); + } else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_ALPHA + && mCurrentAnimator.isRunning()) { + // If we are still animating the fade into pip, then just move the surface and ensure + // we update with the new destination bounds, but don't interrupt the existing animation + // with a new bounds + mCurrentAnimator.setDestinationBounds(endBounds); + } else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_BOUNDS + && mCurrentAnimator.isRunning()) { + mCurrentAnimator.setDestinationBounds(endBounds); + // construct new Rect instances in case they are recycled + mCurrentAnimator.updateEndValue(new Rect(endBounds)); + } else { + mCurrentAnimator.cancel(); + mCurrentAnimator = setupPipTransitionAnimator( + PipTransitionAnimator.ofBounds(leash, startBounds, endBounds, sourceHintRect, + direction)); + } + return mCurrentAnimator; + } + + PipTransitionAnimator getCurrentAnimator() { + return mCurrentAnimator; + } + + private PipTransitionAnimator setupPipTransitionAnimator(PipTransitionAnimator animator) { + animator.setSurfaceTransactionHelper(mSurfaceTransactionHelper); + animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); + animator.setFloatValues(FRACTION_START, FRACTION_END); + animator.setAnimationHandler(mSfAnimationHandlerThreadLocal.get()); + return animator; + } + + /** + * Additional callback interface for PiP animation + */ + public static class PipAnimationCallback { + /** + * Called when PiP animation is started. + */ + public void onPipAnimationStart(PipTransitionAnimator animator) {} + + /** + * Called when PiP animation is ended. + */ + public void onPipAnimationEnd(SurfaceControl.Transaction tx, + PipTransitionAnimator animator) {} + + /** + * Called when PiP animation is cancelled. + */ + public void onPipAnimationCancel(PipTransitionAnimator animator) {} + } + + /** + * Animator for PiP transition animation which supports both alpha and bounds animation. + * @param <T> Type of property to animate, either alpha (float) or bounds (Rect) + */ + public abstract static class PipTransitionAnimator<T> extends ValueAnimator implements + ValueAnimator.AnimatorUpdateListener, + ValueAnimator.AnimatorListener { + private final SurfaceControl mLeash; + private final @AnimationType int mAnimationType; + private final Rect mDestinationBounds = new Rect(); + + protected T mCurrentValue; + protected T mStartValue; + private T mEndValue; + private PipAnimationCallback mPipAnimationCallback; + private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory + mSurfaceControlTransactionFactory; + private PipSurfaceTransactionHelper mSurfaceTransactionHelper; + private @TransitionDirection int mTransitionDirection; + + private PipTransitionAnimator(SurfaceControl leash, @AnimationType int animationType, + Rect destinationBounds, T startValue, T endValue) { + mLeash = leash; + mAnimationType = animationType; + mDestinationBounds.set(destinationBounds); + mStartValue = startValue; + mEndValue = endValue; + addListener(this); + addUpdateListener(this); + mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; + mTransitionDirection = TRANSITION_DIRECTION_NONE; + } + + @Override + public void onAnimationStart(Animator animation) { + mCurrentValue = mStartValue; + onStartTransaction(mLeash, newSurfaceControlTransaction()); + if (mPipAnimationCallback != null) { + mPipAnimationCallback.onPipAnimationStart(this); + } + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + applySurfaceControlTransaction(mLeash, newSurfaceControlTransaction(), + animation.getAnimatedFraction()); + } + + @Override + public void onAnimationEnd(Animator animation) { + mCurrentValue = mEndValue; + final SurfaceControl.Transaction tx = newSurfaceControlTransaction(); + onEndTransaction(mLeash, tx, mTransitionDirection); + if (mPipAnimationCallback != null) { + mPipAnimationCallback.onPipAnimationEnd(tx, this); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + if (mPipAnimationCallback != null) { + mPipAnimationCallback.onPipAnimationCancel(this); + } + } + + @Override public void onAnimationRepeat(Animator animation) {} + + @VisibleForTesting + @AnimationType public int getAnimationType() { + return mAnimationType; + } + + @VisibleForTesting + public PipTransitionAnimator<T> setPipAnimationCallback(PipAnimationCallback callback) { + mPipAnimationCallback = callback; + return this; + } + @VisibleForTesting + @TransitionDirection public int getTransitionDirection() { + return mTransitionDirection; + } + + @VisibleForTesting + public PipTransitionAnimator<T> setTransitionDirection(@TransitionDirection int direction) { + if (direction != TRANSITION_DIRECTION_SAME) { + mTransitionDirection = direction; + } + return this; + } + + T getStartValue() { + return mStartValue; + } + + @VisibleForTesting + public T getEndValue() { + return mEndValue; + } + + Rect getDestinationBounds() { + return mDestinationBounds; + } + + void setDestinationBounds(Rect destinationBounds) { + mDestinationBounds.set(destinationBounds); + if (mAnimationType == ANIM_TYPE_ALPHA) { + onStartTransaction(mLeash, newSurfaceControlTransaction()); + } + } + + void setCurrentValue(T value) { + mCurrentValue = value; + } + + boolean shouldApplyCornerRadius() { + return !isOutPipDirection(mTransitionDirection); + } + + boolean inScaleTransition() { + if (mAnimationType != ANIM_TYPE_BOUNDS) return false; + final int direction = getTransitionDirection(); + return !isInPipDirection(direction) && !isOutPipDirection(direction); + } + + /** + * Updates the {@link #mEndValue}. + * + * NOTE: Do not forget to call {@link #setDestinationBounds(Rect)} for bounds animation. + * This is typically used when we receive a shelf height adjustment during the bounds + * animation. In which case we can update the end bounds and keep the existing animation + * running instead of cancelling it. + */ + public void updateEndValue(T endValue) { + mEndValue = endValue; + } + + SurfaceControl.Transaction newSurfaceControlTransaction() { + return mSurfaceControlTransactionFactory.getTransaction(); + } + + @VisibleForTesting + public void setSurfaceControlTransactionFactory( + PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) { + mSurfaceControlTransactionFactory = factory; + } + + PipSurfaceTransactionHelper getSurfaceTransactionHelper() { + return mSurfaceTransactionHelper; + } + + void setSurfaceTransactionHelper(PipSurfaceTransactionHelper helper) { + mSurfaceTransactionHelper = helper; + } + + void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) {} + + void onEndTransaction(SurfaceControl leash, SurfaceControl.Transaction tx, + @TransitionDirection int transitionDirection) {} + + abstract void applySurfaceControlTransaction(SurfaceControl leash, + SurfaceControl.Transaction tx, float fraction); + + static PipTransitionAnimator<Float> ofAlpha(SurfaceControl leash, + Rect destinationBounds, float startValue, float endValue) { + return new PipTransitionAnimator<Float>(leash, ANIM_TYPE_ALPHA, + destinationBounds, startValue, endValue) { + @Override + void applySurfaceControlTransaction(SurfaceControl leash, + SurfaceControl.Transaction tx, float fraction) { + final float alpha = getStartValue() * (1 - fraction) + getEndValue() * fraction; + setCurrentValue(alpha); + getSurfaceTransactionHelper().alpha(tx, leash, alpha); + tx.apply(); + } + + @Override + void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) { + if (getTransitionDirection() == TRANSITION_DIRECTION_REMOVE_STACK) { + // while removing the pip stack, no extra work needs to be done here. + return; + } + getSurfaceTransactionHelper() + .resetScale(tx, leash, getDestinationBounds()) + .crop(tx, leash, getDestinationBounds()) + .round(tx, leash, shouldApplyCornerRadius()); + tx.show(leash); + tx.apply(); + } + + @Override + public void updateEndValue(Float endValue) { + super.updateEndValue(endValue); + mStartValue = mCurrentValue; + } + }; + } + + static PipTransitionAnimator<Rect> ofBounds(SurfaceControl leash, + Rect startValue, Rect endValue, Rect sourceHintRect, + @PipAnimationController.TransitionDirection int direction) { + // Just for simplicity we'll interpolate between the source rect hint insets and empty + // insets to calculate the window crop + final Rect initialSourceValue; + if (isOutPipDirection(direction)) { + initialSourceValue = new Rect(endValue); + } else { + initialSourceValue = new Rect(startValue); + } + + final Rect sourceHintRectInsets; + if (sourceHintRect == null) { + sourceHintRectInsets = null; + } else { + sourceHintRectInsets = new Rect(sourceHintRect.left - initialSourceValue.left, + sourceHintRect.top - initialSourceValue.top, + initialSourceValue.right - sourceHintRect.right, + initialSourceValue.bottom - sourceHintRect.bottom); + } + final Rect sourceInsets = new Rect(0, 0, 0, 0); + + // construct new Rect instances in case they are recycled + return new PipTransitionAnimator<Rect>(leash, ANIM_TYPE_BOUNDS, + endValue, new Rect(startValue), new Rect(endValue)) { + private final RectEvaluator mRectEvaluator = new RectEvaluator(new Rect()); + private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect()); + + @Override + void applySurfaceControlTransaction(SurfaceControl leash, + SurfaceControl.Transaction tx, float fraction) { + final Rect start = getStartValue(); + final Rect end = getEndValue(); + Rect bounds = mRectEvaluator.evaluate(fraction, start, end); + setCurrentValue(bounds); + if (inScaleTransition() || sourceHintRect == null) { + if (isOutPipDirection(direction)) { + getSurfaceTransactionHelper().scale(tx, leash, end, bounds); + } else { + getSurfaceTransactionHelper().scale(tx, leash, start, bounds); + } + } else { + final Rect insets; + if (isOutPipDirection(direction)) { + insets = mInsetsEvaluator.evaluate(fraction, sourceHintRectInsets, + sourceInsets); + } else { + insets = mInsetsEvaluator.evaluate(fraction, sourceInsets, + sourceHintRectInsets); + } + getSurfaceTransactionHelper().scaleAndCrop(tx, leash, + initialSourceValue, bounds, insets); + } + tx.apply(); + } + + @Override + void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) { + getSurfaceTransactionHelper() + .alpha(tx, leash, 1f) + .round(tx, leash, shouldApplyCornerRadius()); + tx.show(leash); + tx.apply(); + } + + @Override + void onEndTransaction(SurfaceControl leash, SurfaceControl.Transaction tx, + int transitionDirection) { + // NOTE: intentionally does not apply the transaction here. + // this end transaction should get executed synchronously with the final + // WindowContainerTransaction in task organizer + final Rect destBounds = getDestinationBounds(); + getSurfaceTransactionHelper().resetScale(tx, leash, destBounds); + if (transitionDirection == TRANSITION_DIRECTION_LEAVE_PIP) { + // Leaving to fullscreen, reset crop to null. + tx.setPosition(leash, destBounds.left, destBounds.top); + tx.setWindowCrop(leash, 0, 0); + } else { + getSurfaceTransactionHelper().crop(tx, leash, destBounds); + } + } + + @Override + public void updateEndValue(Rect endValue) { + super.updateEndValue(endValue); + if (mStartValue != null && mCurrentValue != null) { + mStartValue.set(mCurrentValue); + } + } + }; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsHandler.java new file mode 100644 index 000000000000..08318186a105 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsHandler.java @@ -0,0 +1,507 @@ +/* + * 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.pip; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.util.TypedValue.COMPLEX_UNIT_DIP; +import static android.view.Surface.ROTATION_0; +import static android.view.Surface.ROTATION_180; + +import android.annotation.NonNull; +import android.app.ActivityTaskManager; +import android.app.ActivityTaskManager.RootTaskInfo; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.RemoteException; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.Size; +import android.util.TypedValue; +import android.view.Display; +import android.view.DisplayInfo; +import android.view.Gravity; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.common.DisplayLayout; + +import java.io.PrintWriter; + +/** + * Handles bounds calculation for PIP on Phone and other form factors, it keeps tracking variant + * state changes originated from Window Manager and is the source of truth for PiP window bounds. + */ +public class PipBoundsHandler { + + private static final String TAG = PipBoundsHandler.class.getSimpleName(); + private static final float INVALID_SNAP_FRACTION = -1f; + + private final @NonNull PipBoundsState mPipBoundsState; + private final PipSnapAlgorithm mSnapAlgorithm; + private final DisplayInfo mDisplayInfo = new DisplayInfo(); + private DisplayLayout mDisplayLayout; + + private float mDefaultAspectRatio; + private float mMinAspectRatio; + private float mMaxAspectRatio; + private int mDefaultStackGravity; + private int mDefaultMinSize; + private Point mScreenEdgeInsets; + private int mCurrentMinSize; + private Size mOverrideMinimalSize; + + private boolean mIsImeShowing; + private int mImeHeight; + private boolean mIsShelfShowing; + private int mShelfHeight; + + public PipBoundsHandler(Context context, @NonNull PipBoundsState pipBoundsState) { + mPipBoundsState = pipBoundsState; + mSnapAlgorithm = new PipSnapAlgorithm(context); + mDisplayLayout = new DisplayLayout(); + reloadResources(context); + // Initialize the aspect ratio to the default aspect ratio. Don't do this in reload + // resources as it would clobber mAspectRatio when entering PiP from fullscreen which + // triggers a configuration change and the resources to be reloaded. + mPipBoundsState.setAspectRatio(mDefaultAspectRatio); + } + + /** + * TODO: move the resources to SysUI package. + */ + private void reloadResources(Context context) { + final Resources res = context.getResources(); + mDefaultAspectRatio = res.getFloat( + com.android.internal.R.dimen.config_pictureInPictureDefaultAspectRatio); + mDefaultStackGravity = res.getInteger( + com.android.internal.R.integer.config_defaultPictureInPictureGravity); + mDefaultMinSize = res.getDimensionPixelSize( + com.android.internal.R.dimen.default_minimal_size_pip_resizable_task); + mCurrentMinSize = mDefaultMinSize; + final String screenEdgeInsetsDpString = res.getString( + com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets); + final Size screenEdgeInsetsDp = !screenEdgeInsetsDpString.isEmpty() + ? Size.parseSize(screenEdgeInsetsDpString) + : null; + mScreenEdgeInsets = screenEdgeInsetsDp == null ? new Point() + : new Point(dpToPx(screenEdgeInsetsDp.getWidth(), res.getDisplayMetrics()), + dpToPx(screenEdgeInsetsDp.getHeight(), res.getDisplayMetrics())); + mMinAspectRatio = res.getFloat( + com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio); + mMaxAspectRatio = res.getFloat( + com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio); + } + + /** + * Sets or update latest {@link DisplayLayout} when new display added or rotation callbacks + * from {@link DisplayController.OnDisplaysChangedListener} + * @param newDisplayLayout latest {@link DisplayLayout} + */ + public void setDisplayLayout(DisplayLayout newDisplayLayout) { + mDisplayLayout.set(newDisplayLayout); + } + + /** + * Get the current saved display info. + */ + public DisplayInfo getDisplayInfo() { + return mDisplayInfo; + } + + /** + * Update the Min edge size for {@link PipSnapAlgorithm} to calculate corresponding bounds + * @param minEdgeSize + */ + public void setMinEdgeSize(int minEdgeSize) { + mCurrentMinSize = minEdgeSize; + } + + /** + * Sets both shelf visibility and its height if applicable. + * @return {@code true} if the internal shelf state is changed, {@code false} otherwise. + */ + public boolean setShelfHeight(boolean shelfVisible, int shelfHeight) { + final boolean shelfShowing = shelfVisible && shelfHeight > 0; + if (shelfShowing == mIsShelfShowing && shelfHeight == mShelfHeight) { + return false; + } + + mIsShelfShowing = shelfVisible; + mShelfHeight = shelfHeight; + return true; + } + + /** + * Responds to IPinnedStackListener on IME visibility change. + */ + public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { + mIsImeShowing = imeVisible; + mImeHeight = imeHeight; + } + + /** + * Responds to IPinnedStackListener on movement bounds change. + * Note that both inset and normal bounds will be calculated here rather than in the caller. + */ + public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, + Rect animatingBounds, DisplayInfo displayInfo) { + getInsetBounds(insetBounds); + final Rect defaultBounds = getDefaultBounds(INVALID_SNAP_FRACTION, null); + normalBounds.set(defaultBounds); + if (animatingBounds.isEmpty()) { + animatingBounds.set(defaultBounds); + } + if (isValidPictureInPictureAspectRatio(mPipBoundsState.getAspectRatio())) { + transformBoundsToAspectRatio(normalBounds, mPipBoundsState.getAspectRatio(), + false /* useCurrentMinEdgeSize */, false /* useCurrentSize */); + } + displayInfo.copyFrom(mDisplayInfo); + } + + /** + * The {@link PipSnapAlgorithm} is couple on display bounds + * @return {@link PipSnapAlgorithm}. + */ + public PipSnapAlgorithm getSnapAlgorithm() { + return mSnapAlgorithm; + } + + public Rect getDisplayBounds() { + return new Rect(0, 0, mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight); + } + + public int getDisplayRotation() { + return mDisplayInfo.rotation; + } + + /** + * Responds to IPinnedStackListener on {@link DisplayInfo} change. + * It will normally follow up with a + * {@link #onMovementBoundsChanged(Rect, Rect, Rect, DisplayInfo)} callback. + */ + public void onDisplayInfoChanged(DisplayInfo displayInfo) { + mDisplayInfo.copyFrom(displayInfo); + } + + /** + * Responds to IPinnedStackListener on configuration change. + */ + public void onConfigurationChanged(Context context) { + reloadResources(context); + } + + /** + * See {@link #getDestinationBounds(Rect, Size, boolean)} + */ + public Rect getDestinationBounds(Rect bounds, Size minimalSize) { + return getDestinationBounds(bounds, minimalSize, false /* useCurrentMinEdgeSize */); + } + + /** + * @return {@link Rect} of the destination PiP window bounds. + */ + public Rect getDestinationBounds(Rect bounds, Size minimalSize, boolean useCurrentMinEdgeSize) { + boolean isReentryBounds = false; + final Rect destinationBounds; + if (bounds == null) { + // Calculating initial entry bounds + final PipBoundsState.PipReentryState state = mPipBoundsState.getReentryState(); + + final Rect defaultBounds; + if (state != null) { + // Restore to reentry bounds. + defaultBounds = getDefaultBounds(state.getSnapFraction(), state.getSize()); + isReentryBounds = true; + } else { + // Get actual default bounds. + defaultBounds = getDefaultBounds(INVALID_SNAP_FRACTION, null /* size */); + mOverrideMinimalSize = minimalSize; + } + + destinationBounds = new Rect(defaultBounds); + } else { + // Just adjusting bounds (e.g. on aspect ratio changed). + destinationBounds = new Rect(bounds); + } + if (isValidPictureInPictureAspectRatio(mPipBoundsState.getAspectRatio())) { + transformBoundsToAspectRatio(destinationBounds, mPipBoundsState.getAspectRatio(), + useCurrentMinEdgeSize, isReentryBounds); + } + return destinationBounds; + } + + public float getDefaultAspectRatio() { + return mDefaultAspectRatio; + } + + public void onOverlayChanged(Context context, Display display) { + mDisplayLayout = new DisplayLayout(context, display); + } + + /** + * Updatest the display info and display layout on rotation change. This is needed even when we + * aren't in PIP because the rotation layout is used to calculate the proper insets for the + * next enter animation into PIP. + */ + public void onDisplayRotationChangedNotInPip(Context context, int toRotation) { + // Update the display layout, note that we have to do this on every rotation even if we + // aren't in PIP since we need to update the display layout to get the right resources + mDisplayLayout.rotateTo(context.getResources(), toRotation); + + // Populate the new {@link #mDisplayInfo}. + // The {@link DisplayInfo} queried from DisplayManager would be the one before rotation, + // therefore, the width/height may require a swap first. + // Moving forward, we should get the new dimensions after rotation from DisplayLayout. + mDisplayInfo.rotation = toRotation; + updateDisplayInfoIfNeeded(); + } + + /** + * Updates the display info, calculating and returning the new stack and movement bounds in the + * new orientation of the device if necessary. + * + * @return {@code true} if internal {@link DisplayInfo} is rotated, {@code false} otherwise. + */ + public boolean onDisplayRotationChanged(Context context, Rect outBounds, Rect oldBounds, + Rect outInsetBounds, + int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) { + // Bail early if the event is not sent to current {@link #mDisplayInfo} + if ((displayId != mDisplayInfo.displayId) || (fromRotation == toRotation)) { + return false; + } + + // Bail early if the pinned task is staled. + final RootTaskInfo pinnedTaskInfo; + try { + pinnedTaskInfo = ActivityTaskManager.getService() + .getRootTaskInfo(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); + if (pinnedTaskInfo == null) return false; + } catch (RemoteException e) { + Log.e(TAG, "Failed to get RootTaskInfo for pinned task", e); + return false; + } + + // Calculate the snap fraction of the current stack along the old movement bounds + final Rect postChangeStackBounds = new Rect(oldBounds); + final float snapFraction = getSnapFraction(postChangeStackBounds); + + // Update the display layout + mDisplayLayout.rotateTo(context.getResources(), toRotation); + + // Populate the new {@link #mDisplayInfo}. + // The {@link DisplayInfo} queried from DisplayManager would be the one before rotation, + // therefore, the width/height may require a swap first. + // Moving forward, we should get the new dimensions after rotation from DisplayLayout. + mDisplayInfo.rotation = toRotation; + updateDisplayInfoIfNeeded(); + + // Calculate the stack bounds in the new orientation based on same fraction along the + // rotated movement bounds. + final Rect postChangeMovementBounds = getMovementBounds(postChangeStackBounds, + false /* adjustForIme */); + mSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds, + snapFraction); + + getInsetBounds(outInsetBounds); + outBounds.set(postChangeStackBounds); + t.setBounds(pinnedTaskInfo.token, outBounds); + return true; + } + + private void updateDisplayInfoIfNeeded() { + final boolean updateNeeded; + if ((mDisplayInfo.rotation == ROTATION_0) || (mDisplayInfo.rotation == ROTATION_180)) { + updateNeeded = (mDisplayInfo.logicalWidth > mDisplayInfo.logicalHeight); + } else { + updateNeeded = (mDisplayInfo.logicalWidth < mDisplayInfo.logicalHeight); + } + if (updateNeeded) { + final int newLogicalHeight = mDisplayInfo.logicalWidth; + mDisplayInfo.logicalWidth = mDisplayInfo.logicalHeight; + mDisplayInfo.logicalHeight = newLogicalHeight; + } + } + + /** + * @return whether the given {@param aspectRatio} is valid. + */ + private boolean isValidPictureInPictureAspectRatio(float aspectRatio) { + return Float.compare(mMinAspectRatio, aspectRatio) <= 0 + && Float.compare(aspectRatio, mMaxAspectRatio) <= 0; + } + + /** + * Sets the current bound with the currently store aspect ratio. + * @param stackBounds + */ + public void transformBoundsToAspectRatio(Rect stackBounds) { + transformBoundsToAspectRatio(stackBounds, mPipBoundsState.getAspectRatio(), + true /* useCurrentMinEdgeSize */, true /* useCurrentSize */); + } + + /** + * Set the current bounds (or the default bounds if there are no current bounds) with the + * specified aspect ratio. + */ + private void transformBoundsToAspectRatio(Rect stackBounds, float aspectRatio, + boolean useCurrentMinEdgeSize, boolean useCurrentSize) { + // Save the snap fraction and adjust the size based on the new aspect ratio. + final float snapFraction = mSnapAlgorithm.getSnapFraction(stackBounds, + getMovementBounds(stackBounds)); + final int minEdgeSize = useCurrentMinEdgeSize ? mCurrentMinSize : mDefaultMinSize; + final Size size; + if (useCurrentMinEdgeSize || useCurrentSize) { + size = mSnapAlgorithm.getSizeForAspectRatio( + new Size(stackBounds.width(), stackBounds.height()), aspectRatio, minEdgeSize); + } else { + size = mSnapAlgorithm.getSizeForAspectRatio(aspectRatio, minEdgeSize, + mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight); + } + + final int left = (int) (stackBounds.centerX() - size.getWidth() / 2f); + final int top = (int) (stackBounds.centerY() - size.getHeight() / 2f); + stackBounds.set(left, top, left + size.getWidth(), top + size.getHeight()); + // apply the override minimal size if applicable, this minimal size is specified by app + if (mOverrideMinimalSize != null) { + transformBoundsToMinimalSize(stackBounds, aspectRatio, mOverrideMinimalSize); + } + mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction); + } + + /** + * Transforms a given bounds to meet the minimal size constraints. + * This function assumes the given {@param stackBounds} qualifies {@param aspectRatio}. + */ + private void transformBoundsToMinimalSize(Rect stackBounds, float aspectRatio, + Size minimalSize) { + if (minimalSize == null) return; + final Size adjustedMinimalSize; + final float minimalSizeAspectRatio = + minimalSize.getWidth() / (float) minimalSize.getHeight(); + if (minimalSizeAspectRatio > aspectRatio) { + // minimal size is wider, fixed the width and increase the height + adjustedMinimalSize = new Size( + minimalSize.getWidth(), (int) (minimalSize.getWidth() / aspectRatio)); + } else { + adjustedMinimalSize = new Size( + (int) (minimalSize.getHeight() * aspectRatio), minimalSize.getHeight()); + } + final Rect containerBounds = new Rect(stackBounds); + Gravity.apply(mDefaultStackGravity, + adjustedMinimalSize.getWidth(), adjustedMinimalSize.getHeight(), + containerBounds, stackBounds); + } + + /** + * @return the default bounds to show the PIP, if a {@param snapFraction} and {@param size} are + * provided, then it will apply the default bounds to the provided snap fraction and size. + */ + private Rect getDefaultBounds(float snapFraction, Size size) { + final Rect defaultBounds = new Rect(); + if (snapFraction != INVALID_SNAP_FRACTION && size != null) { + defaultBounds.set(0, 0, size.getWidth(), size.getHeight()); + final Rect movementBounds = getMovementBounds(defaultBounds); + mSnapAlgorithm.applySnapFraction(defaultBounds, movementBounds, snapFraction); + } else { + final Rect insetBounds = new Rect(); + getInsetBounds(insetBounds); + size = mSnapAlgorithm.getSizeForAspectRatio(mDefaultAspectRatio, + mDefaultMinSize, mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight); + Gravity.apply(mDefaultStackGravity, size.getWidth(), size.getHeight(), insetBounds, + 0, Math.max(mIsImeShowing ? mImeHeight : 0, + mIsShelfShowing ? mShelfHeight : 0), + defaultBounds); + } + return defaultBounds; + } + + /** + * Populates the bounds on the screen that the PIP can be visible in. + */ + protected void getInsetBounds(Rect outRect) { + Rect insets = mDisplayLayout.stableInsets(); + outRect.set(insets.left + mScreenEdgeInsets.x, + insets.top + mScreenEdgeInsets.y, + mDisplayInfo.logicalWidth - insets.right - mScreenEdgeInsets.x, + mDisplayInfo.logicalHeight - insets.bottom - mScreenEdgeInsets.y); + } + + /** + * @return the movement bounds for the given {@param stackBounds} and the current state of the + * controller. + */ + private Rect getMovementBounds(Rect stackBounds) { + return getMovementBounds(stackBounds, true /* adjustForIme */); + } + + /** + * @return the movement bounds for the given {@param stackBounds} and the current state of the + * controller. + */ + private Rect getMovementBounds(Rect stackBounds, boolean adjustForIme) { + final Rect movementBounds = new Rect(); + getInsetBounds(movementBounds); + + // Apply the movement bounds adjustments based on the current state. + mSnapAlgorithm.getMovementBounds(stackBounds, movementBounds, movementBounds, + (adjustForIme && mIsImeShowing) ? mImeHeight : 0); + return movementBounds; + } + + /** + * @return the default snap fraction to apply instead of the default gravity when calculating + * the default stack bounds when first entering PiP. + */ + public float getSnapFraction(Rect stackBounds) { + return mSnapAlgorithm.getSnapFraction(stackBounds, getMovementBounds(stackBounds)); + } + + /** + * Applies the given snap fraction to the given stack bounds. + */ + public void applySnapFraction(Rect stackBounds, float snapFraction) { + final Rect movementBounds = getMovementBounds(stackBounds); + mSnapAlgorithm.applySnapFraction(stackBounds, movementBounds, snapFraction); + } + + /** + * @return the pixels for a given dp value. + */ + private int dpToPx(float dpValue, DisplayMetrics dm) { + return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm); + } + + /** + * Dumps internal states. + */ + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mDisplayInfo=" + mDisplayInfo); + pw.println(innerPrefix + "mDefaultAspectRatio=" + mDefaultAspectRatio); + pw.println(innerPrefix + "mMinAspectRatio=" + mMinAspectRatio); + pw.println(innerPrefix + "mMaxAspectRatio=" + mMaxAspectRatio); + pw.println(innerPrefix + "mDefaultStackGravity=" + mDefaultStackGravity); + pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing); + pw.println(innerPrefix + "mImeHeight=" + mImeHeight); + pw.println(innerPrefix + "mIsShelfShowing=" + mIsShelfShowing); + pw.println(innerPrefix + "mShelfHeight=" + mShelfHeight); + pw.println(innerPrefix + "mSnapAlgorithm" + mSnapAlgorithm); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java new file mode 100644 index 000000000000..aba2a3a29fe2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java @@ -0,0 +1,139 @@ +/* + * 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.pip; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.graphics.Rect; +import android.util.Size; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.PrintWriter; +import java.util.Objects; + +/** + * Singleton source of truth for the current state of PIP bounds. + */ +public final class PipBoundsState { + private static final String TAG = PipBoundsState.class.getSimpleName(); + + private final @NonNull Rect mBounds = new Rect(); + private float mAspectRatio; + private PipReentryState mPipReentryState; + private ComponentName mLastPipComponentName; + + void setBounds(@NonNull Rect bounds) { + mBounds.set(bounds); + } + + @NonNull + public Rect getBounds() { + return new Rect(mBounds); + } + + public void setAspectRatio(float aspectRatio) { + mAspectRatio = aspectRatio; + } + + public float getAspectRatio() { + return mAspectRatio; + } + + /** + * Save the reentry state to restore to when re-entering PIP mode. + * + * TODO(b/169373982): consider refactoring this so that this class alone can use mBounds and + * calculate the snap fraction to save for re-entry. + */ + public void saveReentryState(@NonNull Rect bounds, float fraction) { + mPipReentryState = new PipReentryState(new Size(bounds.width(), bounds.height()), fraction); + } + + /** + * Returns the saved reentry state. + */ + @Nullable + public PipReentryState getReentryState() { + return mPipReentryState; + } + + /** + * Set the last {@link ComponentName} to enter PIP mode. + */ + public void setLastPipComponentName(ComponentName lastPipComponentName) { + final boolean changed = !Objects.equals(mLastPipComponentName, lastPipComponentName); + mLastPipComponentName = lastPipComponentName; + if (changed) { + clearReentryState(); + } + } + + public ComponentName getLastPipComponentName() { + return mLastPipComponentName; + } + + @VisibleForTesting + void clearReentryState() { + mPipReentryState = null; + } + + static final class PipReentryState { + private static final String TAG = PipReentryState.class.getSimpleName(); + + private final @NonNull Size mSize; + private final float mSnapFraction; + + PipReentryState(@NonNull Size size, float snapFraction) { + mSize = size; + mSnapFraction = snapFraction; + } + + @NonNull + Size getSize() { + return mSize; + } + + float getSnapFraction() { + return mSnapFraction; + } + + void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mSize=" + mSize); + pw.println(innerPrefix + "mSnapFraction=" + mSnapFraction); + } + } + + /** + * Dumps internal state. + */ + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mBounds=" + mBounds); + pw.println(innerPrefix + "mLastPipComponentName=" + mLastPipComponentName); + pw.println(innerPrefix + "mAspectRatio=" + mAspectRatio); + if (mPipReentryState == null) { + pw.println(innerPrefix + "mPipReentryState=null"); + } else { + mPipReentryState.dump(pw, innerPrefix); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSnapAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSnapAlgorithm.java new file mode 100644 index 000000000000..820930c463f2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSnapAlgorithm.java @@ -0,0 +1,204 @@ +/* + * 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.pip; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.PointF; +import android.graphics.Rect; +import android.util.Size; + +/** + * Calculates the snap targets and the snap position for the PIP given a position and a velocity. + * All bounds are relative to the display top/left. + */ +public class PipSnapAlgorithm { + + private final float mDefaultSizePercent; + private final float mMinAspectRatioForMinSize; + private final float mMaxAspectRatioForMinSize; + + public PipSnapAlgorithm(Context context) { + Resources res = context.getResources(); + mDefaultSizePercent = res.getFloat( + com.android.internal.R.dimen.config_pictureInPictureDefaultSizePercent); + mMaxAspectRatioForMinSize = res.getFloat( + com.android.internal.R.dimen.config_pictureInPictureAspectRatioLimitForMinSize); + mMinAspectRatioForMinSize = 1f / mMaxAspectRatioForMinSize; + } + + /** + * @return returns a fraction that describes where along the {@param movementBounds} the + * {@param stackBounds} are. If the {@param stackBounds} are not currently on the + * {@param movementBounds} exactly, then they will be snapped to the movement bounds. + * + * The fraction is defined in a clockwise fashion against the {@param movementBounds}: + * + * 0 1 + * 4 +---+ 1 + * | | + * 3 +---+ 2 + * 3 2 + */ + public float getSnapFraction(Rect stackBounds, Rect movementBounds) { + final Rect tmpBounds = new Rect(); + snapRectToClosestEdge(stackBounds, movementBounds, tmpBounds); + final float widthFraction = (float) (tmpBounds.left - movementBounds.left) / + movementBounds.width(); + final float heightFraction = (float) (tmpBounds.top - movementBounds.top) / + movementBounds.height(); + if (tmpBounds.top == movementBounds.top) { + return widthFraction; + } else if (tmpBounds.left == movementBounds.right) { + return 1f + heightFraction; + } else if (tmpBounds.top == movementBounds.bottom) { + return 2f + (1f - widthFraction); + } else { + return 3f + (1f - heightFraction); + } + } + + /** + * Moves the {@param stackBounds} along the {@param movementBounds} to the given snap fraction. + * See {@link #getSnapFraction(Rect, Rect)}. + * + * The fraction is define in a clockwise fashion against the {@param movementBounds}: + * + * 0 1 + * 4 +---+ 1 + * | | + * 3 +---+ 2 + * 3 2 + */ + public void applySnapFraction(Rect stackBounds, Rect movementBounds, float snapFraction) { + if (snapFraction < 1f) { + int offset = movementBounds.left + (int) (snapFraction * movementBounds.width()); + stackBounds.offsetTo(offset, movementBounds.top); + } else if (snapFraction < 2f) { + snapFraction -= 1f; + int offset = movementBounds.top + (int) (snapFraction * movementBounds.height()); + stackBounds.offsetTo(movementBounds.right, offset); + } else if (snapFraction < 3f) { + snapFraction -= 2f; + int offset = movementBounds.left + (int) ((1f - snapFraction) * movementBounds.width()); + stackBounds.offsetTo(offset, movementBounds.bottom); + } else { + snapFraction -= 3f; + int offset = movementBounds.top + (int) ((1f - snapFraction) * movementBounds.height()); + stackBounds.offsetTo(movementBounds.left, offset); + } + } + + /** + * Adjusts {@param movementBoundsOut} so that it is the movement bounds for the given + * {@param stackBounds}. + */ + public void getMovementBounds(Rect stackBounds, Rect insetBounds, Rect movementBoundsOut, + int bottomOffset) { + // Adjust the right/bottom to ensure the stack bounds never goes offscreen + movementBoundsOut.set(insetBounds); + movementBoundsOut.right = Math.max(insetBounds.left, insetBounds.right - + stackBounds.width()); + movementBoundsOut.bottom = Math.max(insetBounds.top, insetBounds.bottom - + stackBounds.height()); + movementBoundsOut.bottom -= bottomOffset; + } + + /** + * @return the size of the PiP at the given {@param aspectRatio}, ensuring that the minimum edge + * is at least {@param minEdgeSize}. + */ + public Size getSizeForAspectRatio(float aspectRatio, float minEdgeSize, int displayWidth, + int displayHeight) { + final int smallestDisplaySize = Math.min(displayWidth, displayHeight); + final int minSize = (int) Math.max(minEdgeSize, smallestDisplaySize * mDefaultSizePercent); + + final int width; + final int height; + if (aspectRatio <= mMinAspectRatioForMinSize || aspectRatio > mMaxAspectRatioForMinSize) { + // Beyond these points, we can just use the min size as the shorter edge + if (aspectRatio <= 1) { + // Portrait, width is the minimum size + width = minSize; + height = Math.round(width / aspectRatio); + } else { + // Landscape, height is the minimum size + height = minSize; + width = Math.round(height * aspectRatio); + } + } else { + // Within these points, we ensure that the bounds fit within the radius of the limits + // at the points + final float widthAtMaxAspectRatioForMinSize = mMaxAspectRatioForMinSize * minSize; + final float radius = PointF.length(widthAtMaxAspectRatioForMinSize, minSize); + height = (int) Math.round(Math.sqrt((radius * radius) / + (aspectRatio * aspectRatio + 1))); + width = Math.round(height * aspectRatio); + } + return new Size(width, height); + } + + /** + * @return the adjusted size so that it conforms to the given aspectRatio, ensuring that the + * minimum edge is at least minEdgeSize. + */ + public Size getSizeForAspectRatio(Size size, float aspectRatio, float minEdgeSize) { + final int smallestSize = Math.min(size.getWidth(), size.getHeight()); + final int minSize = (int) Math.max(minEdgeSize, smallestSize); + + final int width; + final int height; + if (aspectRatio <= 1) { + // Portrait, width is the minimum size. + width = minSize; + height = Math.round(width / aspectRatio); + } else { + // Landscape, height is the minimum size + height = minSize; + width = Math.round(height * aspectRatio); + } + return new Size(width, height); + } + + /** + * Snaps the {@param stackBounds} to the closest edge of the {@param movementBounds} and writes + * the new bounds out to {@param boundsOut}. + */ + public void snapRectToClosestEdge(Rect stackBounds, Rect movementBounds, Rect boundsOut) { + final int boundedLeft = Math.max(movementBounds.left, Math.min(movementBounds.right, + stackBounds.left)); + final int boundedTop = Math.max(movementBounds.top, Math.min(movementBounds.bottom, + stackBounds.top)); + boundsOut.set(stackBounds); + + // Otherwise, just find the closest edge + final int fromLeft = Math.abs(stackBounds.left - movementBounds.left); + final int fromTop = Math.abs(stackBounds.top - movementBounds.top); + final int fromRight = Math.abs(movementBounds.right - stackBounds.left); + final int fromBottom = Math.abs(movementBounds.bottom - stackBounds.top); + final int shortest = Math.min(Math.min(fromLeft, fromRight), Math.min(fromTop, fromBottom)); + if (shortest == fromLeft) { + boundsOut.offsetTo(movementBounds.left, boundedTop); + } else if (shortest == fromTop) { + boundsOut.offsetTo(boundedLeft, movementBounds.top); + } else if (shortest == fromRight) { + boundsOut.offsetTo(movementBounds.right, boundedTop); + } else { + boundsOut.offsetTo(boundedLeft, movementBounds.bottom); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java new file mode 100644 index 000000000000..b9a5536de743 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java @@ -0,0 +1,147 @@ +/* + * 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.pip; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.view.SurfaceControl; + +import com.android.wm.shell.R; + +/** + * Abstracts the common operations on {@link SurfaceControl.Transaction} for PiP transition. + */ +public class PipSurfaceTransactionHelper { + + private final boolean mEnableCornerRadius; + private int mCornerRadius; + + /** for {@link #scale(SurfaceControl.Transaction, SurfaceControl, Rect, Rect)} operation */ + private final Matrix mTmpTransform = new Matrix(); + private final float[] mTmpFloat9 = new float[9]; + private final RectF mTmpSourceRectF = new RectF(); + private final RectF mTmpDestinationRectF = new RectF(); + private final Rect mTmpDestinationRect = new Rect(); + + public PipSurfaceTransactionHelper(Context context) { + final Resources res = context.getResources(); + mEnableCornerRadius = res.getBoolean(R.bool.config_pipEnableRoundCorner); + } + + /** + * Called when display size or font size of settings changed + * + * @param context the current context + */ + public void onDensityOrFontScaleChanged(Context context) { + if (mEnableCornerRadius) { + final Resources res = context.getResources(); + mCornerRadius = res.getDimensionPixelSize(R.dimen.pip_corner_radius); + } + } + + /** + * Operates the alpha on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper alpha(SurfaceControl.Transaction tx, SurfaceControl leash, + float alpha) { + tx.setAlpha(leash, alpha); + return this; + } + + /** + * Operates the crop (and position) on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper crop(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect destinationBounds) { + tx.setWindowCrop(leash, destinationBounds.width(), destinationBounds.height()) + .setPosition(leash, destinationBounds.left, destinationBounds.top); + return this; + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect sourceBounds, Rect destinationBounds) { + mTmpSourceRectF.set(sourceBounds); + mTmpDestinationRectF.set(destinationBounds); + mTmpTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL); + tx.setMatrix(leash, mTmpTransform, mTmpFloat9) + .setPosition(leash, mTmpDestinationRectF.left, mTmpDestinationRectF.top); + return this; + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper scaleAndCrop(SurfaceControl.Transaction tx, + SurfaceControl leash, + Rect sourceBounds, Rect destinationBounds, Rect insets) { + mTmpSourceRectF.set(sourceBounds); + mTmpDestinationRect.set(sourceBounds); + mTmpDestinationRect.inset(insets); + // Scale by the shortest edge and offset such that the top/left of the scaled inset source + // rect aligns with the top/left of the destination bounds + final float scale = sourceBounds.width() <= sourceBounds.height() + ? (float) destinationBounds.width() / sourceBounds.width() + : (float) destinationBounds.height() / sourceBounds.height(); + final float left = destinationBounds.left - insets.left * scale; + final float top = destinationBounds.top - insets.top * scale; + mTmpTransform.setScale(scale, scale); + tx.setMatrix(leash, mTmpTransform, mTmpFloat9) + .setWindowCrop(leash, mTmpDestinationRect) + .setPosition(leash, left, top); + return this; + } + + /** + * Resets the scale (setMatrix) on a given transaction and leash if there's any + * + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper resetScale(SurfaceControl.Transaction tx, + SurfaceControl leash, + Rect destinationBounds) { + tx.setMatrix(leash, Matrix.IDENTITY_MATRIX, mTmpFloat9) + .setPosition(leash, destinationBounds.left, destinationBounds.top); + return this; + } + + /** + * Operates the round corner radius on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper round(SurfaceControl.Transaction tx, SurfaceControl leash, + boolean applyCornerRadius) { + if (mEnableCornerRadius) { + tx.setCornerRadius(leash, applyCornerRadius ? mCornerRadius : 0); + } + return this; + } + + public interface SurfaceControlTransactionFactory { + SurfaceControl.Transaction getTransaction(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java new file mode 100644 index 000000000000..a28477574605 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -0,0 +1,1200 @@ +/* + * 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.pip; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_PIP; +import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString; +import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; +import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_BOUNDS; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_NONE; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_SAME; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; +import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection; +import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.PictureInPictureParams; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.util.Log; +import android.util.Rational; +import android.util.Size; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.WindowManager; +import android.window.TaskOrganizer; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; +import android.window.WindowContainerTransactionCallback; + +import com.android.internal.jank.InteractionJankMonitor; +import com.android.internal.os.SomeArgs; +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.pip.phone.PipMenuActivityController; +import com.android.wm.shell.pip.phone.PipMotionHelper; +import com.android.wm.shell.pip.phone.PipUpdateThread; +import com.android.wm.shell.splitscreen.SplitScreen; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +/** + * Manages PiP tasks such as resize and offset. + * + * This class listens on {@link TaskOrganizer} callbacks for windowing mode change + * both to and from PiP and issues corresponding animation if applicable. + * Normally, we apply series of {@link SurfaceControl.Transaction} when the animator is running + * and files a final {@link WindowContainerTransaction} at the end of the transition. + * + * This class is also responsible for general resize/offset PiP operations within SysUI component, + * see also {@link PipMotionHelper}. + */ +public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, + DisplayController.OnDisplaysChangedListener { + private static final String TAG = PipTaskOrganizer.class.getSimpleName(); + private static final boolean DEBUG = false; + + private static final int MSG_RESIZE_IMMEDIATE = 1; + private static final int MSG_RESIZE_ANIMATE = 2; + private static final int MSG_OFFSET_ANIMATE = 3; + private static final int MSG_FINISH_RESIZE = 4; + private static final int MSG_RESIZE_USER = 5; + + // Not a complete set of states but serves what we want right now. + private enum State { + UNDEFINED(0), + TASK_APPEARED(1), + ENTERING_PIP(2), + ENTERED_PIP(3), + EXITING_PIP(4); + + private final int mStateValue; + + State(int value) { + mStateValue = value; + } + + private boolean isInPip() { + return mStateValue >= TASK_APPEARED.mStateValue + && mStateValue != EXITING_PIP.mStateValue; + } + + /** + * Resize request can be initiated in other component, ignore if we are no longer in PIP, + * still waiting for animation or we're exiting from it. + * + * @return {@code true} if the resize request should be blocked/ignored. + */ + private boolean shouldBlockResizeRequest() { + return mStateValue < ENTERING_PIP.mStateValue + || mStateValue == EXITING_PIP.mStateValue; + } + } + + private final Handler mMainHandler; + private final Handler mUpdateHandler; + private final PipBoundsState mPipBoundsState; + private final PipBoundsHandler mPipBoundsHandler; + private final PipAnimationController mPipAnimationController; + private final PipUiEventLogger mPipUiEventLoggerLogger; + private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>(); + private final int mEnterExitAnimationDuration; + private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; + private final Map<IBinder, Configuration> mInitialState = new HashMap<>(); + private final Optional<SplitScreen> mSplitScreenOptional; + protected final ShellTaskOrganizer mTaskOrganizer; + private SurfaceControlViewHost mPipViewHost; + private SurfaceControl mPipMenuSurface; + + // These callbacks are called on the update thread + private final PipAnimationController.PipAnimationCallback mPipAnimationCallback = + new PipAnimationController.PipAnimationCallback() { + @Override + public void onPipAnimationStart(PipAnimationController.PipTransitionAnimator animator) { + final int direction = animator.getTransitionDirection(); + if (direction == TRANSITION_DIRECTION_TO_PIP) { + InteractionJankMonitor.getInstance().begin( + InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP, 2000); + } + sendOnPipTransitionStarted(direction); + } + + @Override + public void onPipAnimationEnd(SurfaceControl.Transaction tx, + PipAnimationController.PipTransitionAnimator animator) { + final int direction = animator.getTransitionDirection(); + finishResize(tx, animator.getDestinationBounds(), direction, + animator.getAnimationType()); + sendOnPipTransitionFinished(direction); + if (direction == TRANSITION_DIRECTION_TO_PIP) { + InteractionJankMonitor.getInstance().end( + InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP); + } + } + + @Override + public void onPipAnimationCancel(PipAnimationController.PipTransitionAnimator animator) { + sendOnPipTransitionCancelled(animator.getTransitionDirection()); + } + }; + + @SuppressWarnings("unchecked") + private final Handler.Callback mUpdateCallbacks = (msg) -> { + SomeArgs args = (SomeArgs) msg.obj; + Consumer<Rect> updateBoundsCallback = (Consumer<Rect>) args.arg1; + switch (msg.what) { + case MSG_RESIZE_IMMEDIATE: { + Rect toBounds = (Rect) args.arg2; + resizePip(toBounds); + if (updateBoundsCallback != null) { + updateBoundsCallback.accept(toBounds); + } + break; + } + case MSG_RESIZE_ANIMATE: { + Rect currentBounds = (Rect) args.arg2; + Rect toBounds = (Rect) args.arg3; + Rect sourceHintRect = (Rect) args.arg4; + int duration = args.argi2; + animateResizePip(currentBounds, toBounds, sourceHintRect, + args.argi1 /* direction */, duration); + if (updateBoundsCallback != null) { + updateBoundsCallback.accept(toBounds); + } + break; + } + case MSG_OFFSET_ANIMATE: { + Rect originalBounds = (Rect) args.arg2; + final int offset = args.argi1; + final int duration = args.argi2; + offsetPip(originalBounds, 0 /* xOffset */, offset, duration); + Rect toBounds = new Rect(originalBounds); + toBounds.offset(0, offset); + if (updateBoundsCallback != null) { + updateBoundsCallback.accept(toBounds); + } + break; + } + case MSG_FINISH_RESIZE: { + SurfaceControl.Transaction tx = (SurfaceControl.Transaction) args.arg2; + Rect toBounds = (Rect) args.arg3; + finishResize(tx, toBounds, args.argi1 /* direction */, -1); + if (updateBoundsCallback != null) { + updateBoundsCallback.accept(toBounds); + } + break; + } + case MSG_RESIZE_USER: { + Rect startBounds = (Rect) args.arg2; + Rect toBounds = (Rect) args.arg3; + userResizePip(startBounds, toBounds); + if (updateBoundsCallback != null) { + updateBoundsCallback.accept(toBounds); + } + break; + } + } + args.recycle(); + return true; + }; + + private ActivityManager.RunningTaskInfo mTaskInfo; + private WindowContainerToken mToken; + private SurfaceControl mLeash; + private State mState = State.UNDEFINED; + private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS; + private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory + mSurfaceControlTransactionFactory; + private PictureInPictureParams mPictureInPictureParams; + private IntConsumer mOnDisplayIdChangeCallback; + + /** + * If set to {@code true}, the entering animation will be skipped and we will wait for + * {@link #onFixedRotationFinished(int)} callback to actually enter PiP. + */ + private boolean mShouldDeferEnteringPip; + + /** + * If set to {@code true}, no entering PiP transition would be kicked off and most likely + * it's due to the fact that Launcher is handling the transition directly when swiping + * auto PiP-able Activity to home. + * See also {@link #startSwipePipToHome(ComponentName, ActivityInfo, PictureInPictureParams)}. + */ + private boolean mShouldIgnoreEnteringPipTransition; + + public PipTaskOrganizer(Context context, @NonNull PipBoundsState pipBoundsState, + @NonNull PipBoundsHandler boundsHandler, + @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, + Optional<SplitScreen> splitScreenOptional, + @NonNull DisplayController displayController, + @NonNull PipUiEventLogger pipUiEventLogger, + @NonNull ShellTaskOrganizer shellTaskOrganizer) { + mMainHandler = new Handler(Looper.getMainLooper()); + mUpdateHandler = new Handler(PipUpdateThread.get().getLooper(), mUpdateCallbacks); + mPipBoundsState = pipBoundsState; + mPipBoundsHandler = boundsHandler; + mEnterExitAnimationDuration = context.getResources() + .getInteger(R.integer.config_pipResizeAnimationDuration); + mSurfaceTransactionHelper = surfaceTransactionHelper; + mPipAnimationController = new PipAnimationController(mSurfaceTransactionHelper); + mPipUiEventLoggerLogger = pipUiEventLogger; + mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; + mSplitScreenOptional = splitScreenOptional; + mTaskOrganizer = shellTaskOrganizer; + mTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_PIP); + displayController.addDisplayWindowListener(this); + } + + public Handler getUpdateHandler() { + return mUpdateHandler; + } + + public Rect getCurrentOrAnimatingBounds() { + PipAnimationController.PipTransitionAnimator animator = + mPipAnimationController.getCurrentAnimator(); + if (animator != null && animator.isRunning()) { + return new Rect(animator.getDestinationBounds()); + } + return mPipBoundsState.getBounds(); + } + + public boolean isInPip() { + return mState.isInPip(); + } + + public boolean isDeferringEnterPipAnimation() { + return mState.isInPip() && mShouldDeferEnteringPip; + } + + /** + * Registers {@link PipTransitionCallback} to receive transition callbacks. + */ + public void registerPipTransitionCallback(PipTransitionCallback callback) { + mPipTransitionCallbacks.add(callback); + } + + /** + * Registers a callback when a display change has been detected when we enter PiP. + */ + public void registerOnDisplayIdChangeCallback(IntConsumer onDisplayIdChangeCallback) { + mOnDisplayIdChangeCallback = onDisplayIdChangeCallback; + } + + /** + * Sets the preferred animation type for one time. + * This is typically used to set the animation type to + * {@link PipAnimationController#ANIM_TYPE_ALPHA}. + */ + public void setOneShotAnimationType(@PipAnimationController.AnimationType int animationType) { + mOneShotAnimationType = animationType; + } + + /** + * Callback when Launcher starts swipe-pip-to-home operation. + * @return {@link Rect} for destination bounds. + */ + public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, + PictureInPictureParams pictureInPictureParams) { + mShouldIgnoreEnteringPipTransition = true; + sendOnPipTransitionStarted(componentName, TRANSITION_DIRECTION_TO_PIP); + mPipBoundsState.setLastPipComponentName(componentName); + mPipBoundsState.setAspectRatio(getAspectRatioOrDefault(pictureInPictureParams)); + return mPipBoundsHandler.getDestinationBounds(null /* bounds */, + getMinimalSize(activityInfo)); + } + + /** + * Callback when launcher finishes swipe-pip-to-home operation. + * Expect {@link #onTaskAppeared(ActivityManager.RunningTaskInfo, SurfaceControl)} afterwards. + */ + public void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds) { + // do nothing if there is no startSwipePipToHome being called before + if (mShouldIgnoreEnteringPipTransition) { + mPipBoundsState.setBounds(destinationBounds); + } + } + + /** + * Expands PiP to the previous bounds, this is done in two phases using + * {@link WindowContainerTransaction} + * - setActivityWindowingMode to either fullscreen or split-secondary at beginning of the + * transaction. without changing the windowing mode of the Task itself. This makes sure the + * activity render it's final configuration while the Task is still in PiP. + * - setWindowingMode to undefined at the end of transition + * @param animationDurationMs duration in millisecond for the exiting PiP transition + */ + public void exitPip(int animationDurationMs) { + if (!mState.isInPip() || mState == State.EXITING_PIP || mToken == null) { + Log.wtf(TAG, "Not allowed to exitPip in current state" + + " mState=" + mState + " mToken=" + mToken); + return; + } + + final Configuration initialConfig = mInitialState.remove(mToken.asBinder()); + if (initialConfig == null) { + Log.wtf(TAG, "Token not in record, this should not happen mToken=" + mToken); + return; + } + mPipUiEventLoggerLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN); + final boolean orientationDiffers = initialConfig.windowConfiguration.getRotation() + != mPipBoundsHandler.getDisplayRotation(); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + final Rect destinationBounds = initialConfig.windowConfiguration.getBounds(); + final int direction = syncWithSplitScreenBounds(destinationBounds) + ? TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN + : TRANSITION_DIRECTION_LEAVE_PIP; + if (orientationDiffers) { + mState = State.EXITING_PIP; + // Send started callback though animation is ignored. + sendOnPipTransitionStarted(direction); + // Don't bother doing an animation if the display rotation differs or if it's in + // a non-supported windowing mode + applyWindowingModeChangeOnExit(wct, direction); + mTaskOrganizer.applyTransaction(wct); + // Send finished callback though animation is ignored. + sendOnPipTransitionFinished(direction); + } else { + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); + mSurfaceTransactionHelper.scale(tx, mLeash, destinationBounds, + mPipBoundsState.getBounds()); + tx.setWindowCrop(mLeash, destinationBounds.width(), destinationBounds.height()); + // We set to fullscreen here for now, but later it will be set to UNDEFINED for + // the proper windowing mode to take place. See #applyWindowingModeChangeOnExit. + wct.setActivityWindowingMode(mToken, + direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN + ? WINDOWING_MODE_SPLIT_SCREEN_SECONDARY + : WINDOWING_MODE_FULLSCREEN); + wct.setBounds(mToken, destinationBounds); + wct.setBoundsChangeTransaction(mToken, tx); + mTaskOrganizer.applySyncTransaction(wct, new WindowContainerTransactionCallback() { + @Override + public void onTransactionReady(int id, SurfaceControl.Transaction t) { + t.apply(); + // Make sure to grab the latest source hint rect as it could have been updated + // right after applying the windowing mode change. + final Rect sourceHintRect = getValidSourceHintRect(mPictureInPictureParams, + destinationBounds); + scheduleAnimateResizePip(mPipBoundsState.getBounds(), destinationBounds, + sourceHintRect, direction, animationDurationMs, + null /* updateBoundsCallback */); + mState = State.EXITING_PIP; + } + }); + } + } + + private void applyWindowingModeChangeOnExit(WindowContainerTransaction wct, int direction) { + // Reset the final windowing mode. + wct.setWindowingMode(mToken, getOutPipWindowingMode()); + // Simply reset the activity mode set prior to the animation running. + wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); + mSplitScreenOptional.ifPresent(splitScreen -> { + if (direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN) { + wct.reparent(mToken, splitScreen.getSecondaryRoot(), true /* onTop */); + } + }); + } + + /** + * Removes PiP immediately. + */ + public void removePip() { + if (!mState.isInPip() || mToken == null) { + Log.wtf(TAG, "Not allowed to removePip in current state" + + " mState=" + mState + " mToken=" + mToken); + return; + } + + // removePipImmediately is expected when the following animation finishes. + mUpdateHandler.post(() -> mPipAnimationController + .getAnimator(mLeash, mPipBoundsState.getBounds(), 1f, 0f) + .setTransitionDirection(TRANSITION_DIRECTION_REMOVE_STACK) + .setPipAnimationCallback(mPipAnimationCallback) + .setDuration(mEnterExitAnimationDuration) + .start()); + mInitialState.remove(mToken.asBinder()); + mState = State.EXITING_PIP; + } + + private void removePipImmediately() { + try { + // Reset the task bounds first to ensure the activity configuration is reset as well + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setBounds(mToken, null); + mTaskOrganizer.applyTransaction(wct); + + ActivityTaskManager.getService().removeRootTasksInWindowingModes( + new int[]{ WINDOWING_MODE_PINNED }); + } catch (RemoteException e) { + Log.e(TAG, "Failed to remove PiP", e); + } + } + + @Override + public void onTaskAppeared(ActivityManager.RunningTaskInfo info, SurfaceControl leash) { + Objects.requireNonNull(info, "Requires RunningTaskInfo"); + mTaskInfo = info; + mToken = mTaskInfo.token; + mState = State.TASK_APPEARED; + mLeash = leash; + mInitialState.put(mToken.asBinder(), new Configuration(mTaskInfo.configuration)); + mPictureInPictureParams = mTaskInfo.pictureInPictureParams; + mPipBoundsState.setLastPipComponentName(mTaskInfo.topActivity); + + mPipUiEventLoggerLogger.setTaskInfo(mTaskInfo); + mPipUiEventLoggerLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_ENTER); + + // If the displayId of the task is different than what PipBoundsHandler has, then update + // it. This is possible if we entered PiP on an external display. + if (info.displayId != mPipBoundsHandler.getDisplayInfo().displayId + && mOnDisplayIdChangeCallback != null) { + mOnDisplayIdChangeCallback.accept(info.displayId); + } + + if (mShouldIgnoreEnteringPipTransition) { + // animation is finished in the Launcher and here we directly apply the final touch. + applyEnterPipSyncTransaction(mPipBoundsState.getBounds(), + () -> sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP)); + mShouldIgnoreEnteringPipTransition = false; + return; + } + + if (mShouldDeferEnteringPip) { + if (DEBUG) Log.d(TAG, "Defer entering PiP animation, fixed rotation is ongoing"); + // if deferred, hide the surface till fixed rotation is completed + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); + tx.setAlpha(mLeash, 0f); + tx.show(mLeash); + tx.apply(); + return; + } + + mPipBoundsState.setAspectRatio(getAspectRatioOrDefault(mPictureInPictureParams)); + final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds(null /* bounds */, + getMinimalSize(mTaskInfo.topActivityInfo)); + Objects.requireNonNull(destinationBounds, "Missing destination bounds"); + final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); + + if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { + final Rect sourceHintRect = getValidSourceHintRect(info.pictureInPictureParams, + currentBounds); + scheduleAnimateResizePip(currentBounds, destinationBounds, sourceHintRect, + TRANSITION_DIRECTION_TO_PIP, mEnterExitAnimationDuration, + null /* updateBoundsCallback */); + mState = State.ENTERING_PIP; + } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { + enterPipWithAlphaAnimation(destinationBounds, mEnterExitAnimationDuration); + mOneShotAnimationType = ANIM_TYPE_BOUNDS; + } else { + throw new RuntimeException("Unrecognized animation type: " + mOneShotAnimationType); + } + } + + /** + * Returns the source hint rect if it is valid (if provided and is contained by the current + * task bounds). + */ + private Rect getValidSourceHintRect(PictureInPictureParams params, Rect sourceBounds) { + final Rect sourceHintRect = params != null + && params.hasSourceBoundsHint() + ? params.getSourceRectHint() + : null; + if (sourceHintRect != null && sourceBounds.contains(sourceHintRect)) { + return sourceHintRect; + } + return null; + } + + private void enterPipWithAlphaAnimation(Rect destinationBounds, long durationMs) { + // If we are fading the PIP in, then we should move the pip to the final location as + // soon as possible, but set the alpha immediately since the transaction can take a + // while to process + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); + tx.setAlpha(mLeash, 0f); + tx.apply(); + applyEnterPipSyncTransaction(destinationBounds, () -> { + mUpdateHandler.post(() -> mPipAnimationController + .getAnimator(mLeash, destinationBounds, 0f, 1f) + .setTransitionDirection(TRANSITION_DIRECTION_TO_PIP) + .setPipAnimationCallback(mPipAnimationCallback) + .setDuration(durationMs) + .start()); + // mState is set right after the animation is kicked off to block any resize + // requests such as offsetPip that may have been called prior to the transition. + mState = State.ENTERING_PIP; + }); + } + + private void applyEnterPipSyncTransaction(Rect destinationBounds, Runnable runnable) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); + wct.setBounds(mToken, destinationBounds); + wct.scheduleFinishEnterPip(mToken, destinationBounds); + mTaskOrganizer.applySyncTransaction(wct, new WindowContainerTransactionCallback() { + @Override + public void onTransactionReady(int id, SurfaceControl.Transaction t) { + t.apply(); + if (runnable != null) { + runnable.run(); + } + } + }); + } + + private void sendOnPipTransitionStarted( + @PipAnimationController.TransitionDirection int direction) { + sendOnPipTransitionStarted(mTaskInfo.baseActivity, direction); + } + + private void sendOnPipTransitionStarted(ComponentName componentName, + @PipAnimationController.TransitionDirection int direction) { + if (direction == TRANSITION_DIRECTION_TO_PIP) { + mState = State.ENTERING_PIP; + } + final Rect pipBounds = mPipBoundsState.getBounds(); + runOnMainHandler(() -> { + for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { + final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); + callback.onPipTransitionStarted(componentName, direction, pipBounds); + } + }); + } + + private void sendOnPipTransitionFinished( + @PipAnimationController.TransitionDirection int direction) { + if (direction == TRANSITION_DIRECTION_TO_PIP) { + mState = State.ENTERED_PIP; + } + runOnMainHandler(() -> { + for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { + final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); + callback.onPipTransitionFinished(mTaskInfo.baseActivity, direction); + } + }); + } + + private void sendOnPipTransitionCancelled( + @PipAnimationController.TransitionDirection int direction) { + runOnMainHandler(() -> { + for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { + final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); + callback.onPipTransitionCanceled(mTaskInfo.baseActivity, direction); + } + }); + } + + private void runOnMainHandler(Runnable r) { + if (Looper.getMainLooper() == Looper.myLooper()) { + r.run(); + } else { + mMainHandler.post(r); + } + } + + /** + * Setup the ViewHost and attach the provided menu view to the ViewHost. + * @return The input token belonging to the PipMenuView. + */ + public IBinder attachPipMenuViewHost(View menuView, WindowManager.LayoutParams lp) { + if (mPipMenuSurface != null) { + Log.e(TAG, "PIP Menu View already created and attached."); + return null; + } + + if (mLeash == null) { + Log.e(TAG, "PiP Leash is not yet ready."); + return null; + } + + if (Looper.getMainLooper() != Looper.myLooper()) { + throw new RuntimeException("PipMenuView needs to be attached on the main thread."); + } + final Context context = menuView.getContext(); + mPipViewHost = new SurfaceControlViewHost(context, context.getDisplay(), + (android.os.Binder) null); + mPipMenuSurface = mPipViewHost.getSurfacePackage().getSurfaceControl(); + SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); + transaction.reparent(mPipMenuSurface, mLeash); + transaction.show(mPipMenuSurface); + transaction.setRelativeLayer(mPipMenuSurface, mLeash, 1); + transaction.apply(); + mPipViewHost.setView(menuView, lp); + + return mPipViewHost.getSurfacePackage().getInputToken(); + } + + + /** + * Releases the PIP Menu's View host, remove it from PIP task surface. + */ + public void detachPipMenuViewHost() { + if (mPipMenuSurface != null) { + SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); + transaction.remove(mPipMenuSurface); + transaction.apply(); + mPipMenuSurface = null; + mPipViewHost = null; + } + } + + /** + * Return whether the PiP Menu has been attached to the leash yet. + */ + public boolean isPipMenuViewHostAttached() { + return mPipViewHost != null; + } + + + /** + * Note that dismissing PiP is now originated from SystemUI, see {@link #exitPip(int)}. + * Meanwhile this callback is invoked whenever the task is removed. For instance: + * - as a result of removeRootTasksInWindowingModes from WM + * - activity itself is died + * Nevertheless, we simply update the internal state here as all the heavy lifting should + * have been done in WM. + */ + @Override + public void onTaskVanished(ActivityManager.RunningTaskInfo info) { + if (!mState.isInPip()) { + return; + } + final WindowContainerToken token = info.token; + Objects.requireNonNull(token, "Requires valid WindowContainerToken"); + if (token.asBinder() != mToken.asBinder()) { + Log.wtf(TAG, "Unrecognized token: " + token); + return; + } + mShouldDeferEnteringPip = false; + mShouldIgnoreEnteringPipTransition = false; + mPictureInPictureParams = null; + mState = State.UNDEFINED; + mPipUiEventLoggerLogger.setTaskInfo(null); + } + + @Override + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) { + Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken"); + mPipBoundsState.setLastPipComponentName(info.topActivity); + final PictureInPictureParams newParams = info.pictureInPictureParams; + if (newParams == null || !applyPictureInPictureParams(newParams)) { + Log.d(TAG, "Ignored onTaskInfoChanged with PiP param: " + newParams); + return; + } + // Aspect ratio changed, re-calculate destination bounds. + final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds( + mPipBoundsState.getBounds(), getMinimalSize(info.topActivityInfo), + true /* userCurrentMinEdgeSize */); + Objects.requireNonNull(destinationBounds, "Missing destination bounds"); + scheduleAnimateResizePip(destinationBounds, mEnterExitAnimationDuration, + null /* updateBoundsCallback */); + } + + @Override + public void onFixedRotationStarted(int displayId, int newRotation) { + mShouldDeferEnteringPip = true; + } + + @Override + public void onFixedRotationFinished(int displayId) { + if (mShouldDeferEnteringPip && mState.isInPip()) { + final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds( + null /* bounds */, getMinimalSize(mTaskInfo.topActivityInfo)); + // schedule a regular animation to ensure all the callbacks are still being sent + enterPipWithAlphaAnimation(destinationBounds, 0 /* durationMs */); + } + mShouldDeferEnteringPip = false; + } + + /** + * Called when display size or font size of settings changed + */ + public void onDensityOrFontScaleChanged(Context context) { + mSurfaceTransactionHelper.onDensityOrFontScaleChanged(context); + } + + /** + * TODO(b/152809058): consolidate the display info handling logic in SysUI + * + * @param destinationBoundsOut the current destination bounds will be populated to this param + */ + @SuppressWarnings("unchecked") + public void onMovementBoundsChanged(Rect destinationBoundsOut, boolean fromRotation, + boolean fromImeAdjustment, boolean fromShelfAdjustment, + WindowContainerTransaction wct) { + final PipAnimationController.PipTransitionAnimator animator = + mPipAnimationController.getCurrentAnimator(); + if (animator == null || !animator.isRunning() + || animator.getTransitionDirection() != TRANSITION_DIRECTION_TO_PIP) { + if (mState.isInPip() && fromRotation) { + // If we are rotating while there is a current animation, immediately cancel the + // animation (remove the listeners so we don't trigger the normal finish resize + // call that should only happen on the update thread) + int direction = TRANSITION_DIRECTION_NONE; + if (animator != null) { + direction = animator.getTransitionDirection(); + animator.removeAllUpdateListeners(); + animator.removeAllListeners(); + animator.cancel(); + // Do notify the listeners that this was canceled + sendOnPipTransitionCancelled(direction); + sendOnPipTransitionFinished(direction); + } + mPipBoundsState.setBounds(destinationBoundsOut); + + // Create a reset surface transaction for the new bounds and update the window + // container transaction + final SurfaceControl.Transaction tx = createFinishResizeSurfaceTransaction( + destinationBoundsOut); + prepareFinishResizeTransaction(destinationBoundsOut, direction, tx, wct); + } else { + // There could be an animation on-going. If there is one on-going, last-reported + // bounds isn't yet updated. We'll use the animator's bounds instead. + if (animator != null && animator.isRunning()) { + if (!animator.getDestinationBounds().isEmpty()) { + destinationBoundsOut.set(animator.getDestinationBounds()); + } + } else { + if (!mPipBoundsState.getBounds().isEmpty()) { + destinationBoundsOut.set(mPipBoundsState.getBounds()); + } + } + } + return; + } + + final Rect currentDestinationBounds = animator.getDestinationBounds(); + destinationBoundsOut.set(currentDestinationBounds); + if (!fromImeAdjustment && !fromShelfAdjustment + && mPipBoundsHandler.getDisplayBounds().contains(currentDestinationBounds)) { + // no need to update the destination bounds, bail early + return; + } + + final Rect newDestinationBounds = mPipBoundsHandler.getDestinationBounds(null /* bounds */, + getMinimalSize(mTaskInfo.topActivityInfo)); + if (newDestinationBounds.equals(currentDestinationBounds)) return; + if (animator.getAnimationType() == ANIM_TYPE_BOUNDS) { + animator.updateEndValue(newDestinationBounds); + } + animator.setDestinationBounds(newDestinationBounds); + destinationBoundsOut.set(newDestinationBounds); + } + + /** + * @return {@code true} if the aspect ratio is changed since no other parameters within + * {@link PictureInPictureParams} would affect the bounds. + */ + private boolean applyPictureInPictureParams(@NonNull PictureInPictureParams params) { + final Rational currentAspectRatio = + mPictureInPictureParams != null ? mPictureInPictureParams.getAspectRatioRational() + : null; + final boolean aspectRatioChanged = !Objects.equals(currentAspectRatio, + params.getAspectRatioRational()); + mPictureInPictureParams = params; + if (aspectRatioChanged) { + mPipBoundsState.setAspectRatio(params.getAspectRatio()); + } + return aspectRatioChanged; + } + + /** + * Animates resizing of the pinned stack given the duration. + */ + public void scheduleAnimateResizePip(Rect toBounds, int duration, + Consumer<Rect> updateBoundsCallback) { + if (mShouldDeferEnteringPip) { + Log.d(TAG, "skip scheduleAnimateResizePip, entering pip deferred"); + return; + } + scheduleAnimateResizePip(mPipBoundsState.getBounds(), toBounds, null /* sourceHintRect */, + TRANSITION_DIRECTION_NONE, duration, updateBoundsCallback); + } + + private void scheduleAnimateResizePip(Rect currentBounds, Rect destinationBounds, + Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, + int durationMs, Consumer<Rect> updateBoundsCallback) { + if (!mState.isInPip()) { + // TODO: tend to use shouldBlockResizeRequest here as well but need to consider + // the fact that when in exitPip, scheduleAnimateResizePip is executed in the window + // container transaction callback and we want to set the mState immediately. + return; + } + + SomeArgs args = SomeArgs.obtain(); + args.arg1 = updateBoundsCallback; + args.arg2 = currentBounds; + args.arg3 = destinationBounds; + args.arg4 = sourceHintRect; + args.argi1 = direction; + args.argi2 = durationMs; + mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESIZE_ANIMATE, args)); + } + + /** + * Directly perform manipulation/resize on the leash. This will not perform any + * {@link WindowContainerTransaction} until {@link #scheduleFinishResizePip} is called. + */ + public void scheduleResizePip(Rect toBounds, Consumer<Rect> updateBoundsCallback) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = updateBoundsCallback; + args.arg2 = toBounds; + mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESIZE_IMMEDIATE, args)); + } + + /** + * Directly perform a scaled matrix transformation on the leash. This will not perform any + * {@link WindowContainerTransaction} until {@link #scheduleFinishResizePip} is called. + */ + public void scheduleUserResizePip(Rect startBounds, Rect toBounds, + Consumer<Rect> updateBoundsCallback) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = updateBoundsCallback; + args.arg2 = startBounds; + args.arg3 = toBounds; + mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESIZE_USER, args)); + } + + /** + * Finish an intermediate resize operation. This is expected to be called after + * {@link #scheduleResizePip}. + */ + public void scheduleFinishResizePip(Rect destinationBounds) { + scheduleFinishResizePip(destinationBounds, null /* updateBoundsCallback */); + } + + /** + * Same as {@link #scheduleFinishResizePip} but with a callback. + */ + public void scheduleFinishResizePip(Rect destinationBounds, + Consumer<Rect> updateBoundsCallback) { + scheduleFinishResizePip(destinationBounds, TRANSITION_DIRECTION_NONE, updateBoundsCallback); + } + + private void scheduleFinishResizePip(Rect destinationBounds, + @PipAnimationController.TransitionDirection int direction, + Consumer<Rect> updateBoundsCallback) { + if (mState.shouldBlockResizeRequest()) { + return; + } + + SomeArgs args = SomeArgs.obtain(); + args.arg1 = updateBoundsCallback; + args.arg2 = createFinishResizeSurfaceTransaction( + destinationBounds); + args.arg3 = destinationBounds; + args.argi1 = direction; + mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_FINISH_RESIZE, args)); + } + + private SurfaceControl.Transaction createFinishResizeSurfaceTransaction( + Rect destinationBounds) { + final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); + mSurfaceTransactionHelper + .crop(tx, mLeash, destinationBounds) + .resetScale(tx, mLeash, destinationBounds) + .round(tx, mLeash, mState.isInPip()); + return tx; + } + + /** + * Offset the PiP window by a given offset on Y-axis, triggered also from screen rotation. + */ + public void scheduleOffsetPip(Rect originalBounds, int offset, int duration, + Consumer<Rect> updateBoundsCallback) { + if (mState.shouldBlockResizeRequest()) { + return; + } + if (mShouldDeferEnteringPip) { + Log.d(TAG, "skip scheduleOffsetPip, entering pip deferred"); + return; + } + SomeArgs args = SomeArgs.obtain(); + args.arg1 = updateBoundsCallback; + args.arg2 = originalBounds; + // offset would be zero if triggered from screen rotation. + args.argi1 = offset; + args.argi2 = duration; + mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_OFFSET_ANIMATE, args)); + } + + private void offsetPip(Rect originalBounds, int xOffset, int yOffset, int durationMs) { + if (Looper.myLooper() != mUpdateHandler.getLooper()) { + throw new RuntimeException("Callers should call scheduleOffsetPip() instead of this " + + "directly"); + } + if (mTaskInfo == null) { + Log.w(TAG, "mTaskInfo is not set"); + return; + } + final Rect destinationBounds = new Rect(originalBounds); + destinationBounds.offset(xOffset, yOffset); + animateResizePip(originalBounds, destinationBounds, null /* sourceHintRect */, + TRANSITION_DIRECTION_SAME, durationMs); + } + + private void resizePip(Rect destinationBounds) { + if (Looper.myLooper() != mUpdateHandler.getLooper()) { + throw new RuntimeException("Callers should call scheduleResizePip() instead of this " + + "directly"); + } + // Could happen when exitPip + if (mToken == null || mLeash == null) { + Log.w(TAG, "Abort animation, invalid leash"); + return; + } + mPipBoundsState.setBounds(destinationBounds); + final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); + mSurfaceTransactionHelper + .crop(tx, mLeash, destinationBounds) + .round(tx, mLeash, mState.isInPip()); + tx.apply(); + } + + private void userResizePip(Rect startBounds, Rect destinationBounds) { + if (Looper.myLooper() != mUpdateHandler.getLooper()) { + throw new RuntimeException("Callers should call scheduleUserResizePip() instead of " + + "this directly"); + } + // Could happen when exitPip + if (mToken == null || mLeash == null) { + Log.w(TAG, "Abort animation, invalid leash"); + return; + } + + if (startBounds.isEmpty() || destinationBounds.isEmpty()) { + Log.w(TAG, "Attempted to user resize PIP to or from empty bounds, aborting."); + return; + } + + final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); + mSurfaceTransactionHelper.scale(tx, mLeash, startBounds, destinationBounds); + tx.apply(); + } + + private void finishResize(SurfaceControl.Transaction tx, Rect destinationBounds, + @PipAnimationController.TransitionDirection int direction, + @PipAnimationController.AnimationType int type) { + if (Looper.myLooper() != mUpdateHandler.getLooper()) { + throw new RuntimeException("Callers should call scheduleResizePip() instead of this " + + "directly"); + } + mPipBoundsState.setBounds(destinationBounds); + if (direction == TRANSITION_DIRECTION_REMOVE_STACK) { + removePipImmediately(); + return; + } else if (isInPipDirection(direction) && type == ANIM_TYPE_ALPHA) { + return; + } + + WindowContainerTransaction wct = new WindowContainerTransaction(); + prepareFinishResizeTransaction(destinationBounds, direction, tx, wct); + applyFinishBoundsResize(wct, direction); + runOnMainHandler(() -> { + if (mPipViewHost != null) { + mPipViewHost.relayout(PipMenuActivityController.getPipMenuLayoutParams( + destinationBounds.width(), destinationBounds.height())); + } + }); + } + + private void prepareFinishResizeTransaction(Rect destinationBounds, + @PipAnimationController.TransitionDirection int direction, + SurfaceControl.Transaction tx, + WindowContainerTransaction wct) { + final Rect taskBounds; + if (isInPipDirection(direction)) { + // If we are animating from fullscreen using a bounds animation, then reset the + // activity windowing mode set by WM, and set the task bounds to the final bounds + taskBounds = destinationBounds; + wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); + wct.scheduleFinishEnterPip(mToken, destinationBounds); + } else if (isOutPipDirection(direction)) { + // If we are animating to fullscreen, then we need to reset the override bounds + // on the task to ensure that the task "matches" the parent's bounds. + taskBounds = (direction == TRANSITION_DIRECTION_LEAVE_PIP) + ? null : destinationBounds; + applyWindowingModeChangeOnExit(wct, direction); + } else { + // Just a resize in PIP + taskBounds = destinationBounds; + } + + wct.setBounds(mToken, taskBounds); + wct.setBoundsChangeTransaction(mToken, tx); + } + + /** + * Applies the window container transaction to finish a bounds resize. + * + * Called by {@link #finishResize(SurfaceControl.Transaction, Rect, int, int)}} once it has + * finished preparing the transaction. It allows subclasses to modify the transaction before + * applying it. + */ + public void applyFinishBoundsResize(@NonNull WindowContainerTransaction wct, + @PipAnimationController.TransitionDirection int direction) { + mTaskOrganizer.applyTransaction(wct); + } + + /** + * The windowing mode to restore to when resizing out of PIP direction. Defaults to undefined + * and can be overridden to restore to an alternate windowing mode. + */ + public int getOutPipWindowingMode() { + // By default, simply reset the windowing mode to undefined. + return WINDOWING_MODE_UNDEFINED; + } + + private void animateResizePip(Rect currentBounds, Rect destinationBounds, Rect sourceHintRect, + @PipAnimationController.TransitionDirection int direction, int durationMs) { + if (Looper.myLooper() != mUpdateHandler.getLooper()) { + throw new RuntimeException("Callers should call scheduleAnimateResizePip() instead of " + + "this directly"); + } + // Could happen when exitPip + if (mToken == null || mLeash == null) { + Log.w(TAG, "Abort animation, invalid leash"); + return; + } + mPipAnimationController + .getAnimator(mLeash, currentBounds, destinationBounds, sourceHintRect, direction) + .setTransitionDirection(direction) + .setPipAnimationCallback(mPipAnimationCallback) + .setDuration(durationMs) + .start(); + } + + private Size getMinimalSize(ActivityInfo activityInfo) { + if (activityInfo == null || activityInfo.windowLayout == null) { + return null; + } + final ActivityInfo.WindowLayout windowLayout = activityInfo.windowLayout; + // -1 will be populated if an activity specifies defaultWidth/defaultHeight in <layout> + // without minWidth/minHeight + if (windowLayout.minWidth > 0 && windowLayout.minHeight > 0) { + return new Size(windowLayout.minWidth, windowLayout.minHeight); + } + return null; + } + + private float getAspectRatioOrDefault(@Nullable PictureInPictureParams params) { + return params == null || !params.hasSetAspectRatio() + ? mPipBoundsHandler.getDefaultAspectRatio() + : params.getAspectRatio(); + } + + /** + * Sync with {@link SplitScreen} on destination bounds if PiP is going to split screen. + * + * @param destinationBoundsOut contain the updated destination bounds if applicable + * @return {@code true} if destinationBounds is altered for split screen + */ + private boolean syncWithSplitScreenBounds(Rect destinationBoundsOut) { + if (!mSplitScreenOptional.isPresent()) { + return false; + } + + SplitScreen splitScreen = mSplitScreenOptional.get(); + if (!splitScreen.isDividerVisible()) { + // fail early if system is not in split screen mode + return false; + } + + // PiP window will go to split-secondary mode instead of fullscreen, populates the + // split screen bounds here. + destinationBoundsOut.set(splitScreen.getDividerView() + .getNonMinimizedSplitScreenSecondaryBounds()); + return true; + } + + /** + * Dumps internal states. + */ + @Override + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mTaskInfo=" + mTaskInfo); + pw.println(innerPrefix + "mToken=" + mToken + + " binder=" + (mToken != null ? mToken.asBinder() : null)); + pw.println(innerPrefix + "mLeash=" + mLeash); + pw.println(innerPrefix + "mState=" + mState); + pw.println(innerPrefix + "mOneShotAnimationType=" + mOneShotAnimationType); + pw.println(innerPrefix + "mPictureInPictureParams=" + mPictureInPictureParams); + pw.println(innerPrefix + "mInitialState:"); + for (Map.Entry<IBinder, Configuration> e : mInitialState.entrySet()) { + pw.println(innerPrefix + " binder=" + e.getKey() + + " winConfig=" + e.getValue().windowConfiguration); + } + } + + @Override + public String toString() { + return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_PIP); + } + + /** + * Callback interface for PiP transitions (both from and to PiP mode) + */ + public interface PipTransitionCallback { + /** + * Callback when the pip transition is started. + */ + void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds); + + /** + * Callback when the pip transition is finished. + */ + void onPipTransitionFinished(ComponentName activity, int direction); + + /** + * Callback when the pip transition is cancelled. + */ + void onPipTransitionCanceled(ComponentName activity, int direction); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUiEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUiEventLogger.java new file mode 100644 index 000000000000..de3bb2950c0a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUiEventLogger.java @@ -0,0 +1,114 @@ +/* + * 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.pip; + +import android.app.TaskInfo; +import android.content.pm.PackageManager; + +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; + +/** + * Helper class that ends PiP log to UiEvent, see also go/uievent + */ +public class PipUiEventLogger { + + private static final int INVALID_PACKAGE_UID = -1; + + private final UiEventLogger mUiEventLogger; + private final PackageManager mPackageManager; + + private String mPackageName; + private int mPackageUid = INVALID_PACKAGE_UID; + + public PipUiEventLogger(UiEventLogger uiEventLogger, PackageManager packageManager) { + mUiEventLogger = uiEventLogger; + mPackageManager = packageManager; + } + + public void setTaskInfo(TaskInfo taskInfo) { + if (taskInfo == null) { + mPackageName = null; + mPackageUid = INVALID_PACKAGE_UID; + } else { + mPackageName = taskInfo.topActivity.getPackageName(); + mPackageUid = getUid(mPackageName, taskInfo.userId); + } + } + + /** + * Sends log via UiEvent, reference go/uievent for how to debug locally + */ + public void log(PipUiEventEnum event) { + if (mPackageName == null || mPackageUid == INVALID_PACKAGE_UID) { + return; + } + mUiEventLogger.log(event, mPackageUid, mPackageName); + } + + private int getUid(String packageName, int userId) { + int uid = INVALID_PACKAGE_UID; + try { + uid = mPackageManager.getApplicationInfoAsUser( + packageName, 0 /* ApplicationInfoFlags */, userId).uid; + } catch (PackageManager.NameNotFoundException e) { + // do nothing. + } + return uid; + } + + /** + * Enums for logging the PiP events to UiEvent + */ + public enum PipUiEventEnum implements UiEventLogger.UiEventEnum { + @UiEvent(doc = "Activity enters picture-in-picture mode") + PICTURE_IN_PICTURE_ENTER(603), + + @UiEvent(doc = "Expands from picture-in-picture to fullscreen") + PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN(604), + + @UiEvent(doc = "Removes picture-in-picture by tap close button") + PICTURE_IN_PICTURE_TAP_TO_REMOVE(605), + + @UiEvent(doc = "Removes picture-in-picture by drag to dismiss area") + PICTURE_IN_PICTURE_DRAG_TO_REMOVE(606), + + @UiEvent(doc = "Shows picture-in-picture menu") + PICTURE_IN_PICTURE_SHOW_MENU(607), + + @UiEvent(doc = "Hides picture-in-picture menu") + PICTURE_IN_PICTURE_HIDE_MENU(608), + + @UiEvent(doc = "Changes the aspect ratio of picture-in-picture window. This is inherited" + + " from previous Tron-based logging and currently not in use.") + PICTURE_IN_PICTURE_CHANGE_ASPECT_RATIO(609), + + @UiEvent(doc = "User resize of the picture-in-picture window") + PICTURE_IN_PICTURE_RESIZE(610); + + private final int mId; + + PipUiEventEnum(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java new file mode 100644 index 000000000000..18b6922f3067 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java @@ -0,0 +1,274 @@ +/* + * 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.pip.phone; + +import android.annotation.NonNull; +import android.content.Context; +import android.graphics.Rect; +import android.graphics.Region; +import android.os.Bundle; +import android.os.Handler; +import android.os.RemoteException; +import android.view.MagnificationSpec; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityWindowInfo; +import android.view.accessibility.IAccessibilityInteractionConnection; +import android.view.accessibility.IAccessibilityInteractionConnectionCallback; + +import com.android.wm.shell.R; +import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipSnapAlgorithm; +import com.android.wm.shell.pip.PipTaskOrganizer; + +import java.util.ArrayList; +import java.util.List; + +/** + * Expose the touch actions to accessibility as if this object were a window with a single view. + * That pseudo-view exposes all of the actions this object can perform. + */ +public class PipAccessibilityInteractionConnection + extends IAccessibilityInteractionConnection.Stub { + + public interface AccessibilityCallbacks { + void onAccessibilityShowMenu(); + } + + private static final long ACCESSIBILITY_NODE_ID = 1; + private List<AccessibilityNodeInfo> mAccessibilityNodeInfoList; + + private Context mContext; + private Handler mHandler; + private final @NonNull PipBoundsState mPipBoundsState; + private PipMotionHelper mMotionHelper; + private PipTaskOrganizer mTaskOrganizer; + private PipSnapAlgorithm mSnapAlgorithm; + private Runnable mUpdateMovementBoundCallback; + private AccessibilityCallbacks mCallbacks; + + private final Rect mNormalBounds = new Rect(); + private final Rect mExpandedBounds = new Rect(); + private final Rect mNormalMovementBounds = new Rect(); + private final Rect mExpandedMovementBounds = new Rect(); + private Rect mTmpBounds = new Rect(); + + public PipAccessibilityInteractionConnection(Context context, + @NonNull PipBoundsState pipBoundsState, PipMotionHelper motionHelper, + PipTaskOrganizer taskOrganizer, PipSnapAlgorithm snapAlgorithm, + AccessibilityCallbacks callbacks, Runnable updateMovementBoundCallback, + Handler handler) { + mContext = context; + mHandler = handler; + mPipBoundsState = pipBoundsState; + mMotionHelper = motionHelper; + mTaskOrganizer = taskOrganizer; + mSnapAlgorithm = snapAlgorithm; + mUpdateMovementBoundCallback = updateMovementBoundCallback; + mCallbacks = callbacks; + } + + @Override + public void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, + Region interactiveRegion, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid, MagnificationSpec spec, Bundle args) { + try { + callback.setFindAccessibilityNodeInfosResult( + (accessibilityNodeId == AccessibilityNodeInfo.ROOT_NODE_ID) + ? getNodeList() : null, interactionId); + } catch (RemoteException re) { + /* best effort - ignore */ + } + } + + @Override + public void performAccessibilityAction(long accessibilityNodeId, int action, + Bundle arguments, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid) { + // We only support one view. A request for anything else is invalid + boolean result = false; + if (accessibilityNodeId == AccessibilityNodeInfo.ROOT_NODE_ID) { + + // R constants are not final so this cannot be put in the switch-case. + if (action == R.id.action_pip_resize) { + if (mMotionHelper.getBounds().width() == mNormalBounds.width() + && mMotionHelper.getBounds().height() == mNormalBounds.height()) { + setToExpandedBounds(); + } else { + setToNormalBounds(); + } + result = true; + } else { + switch (action) { + case AccessibilityNodeInfo.ACTION_CLICK: + mHandler.post(() -> { + mCallbacks.onAccessibilityShowMenu(); + }); + result = true; + break; + case AccessibilityNodeInfo.ACTION_DISMISS: + mMotionHelper.dismissPip(); + result = true; + break; + case com.android.internal.R.id.accessibilityActionMoveWindow: + int newX = arguments.getInt( + AccessibilityNodeInfo.ACTION_ARGUMENT_MOVE_WINDOW_X); + int newY = arguments.getInt( + AccessibilityNodeInfo.ACTION_ARGUMENT_MOVE_WINDOW_Y); + Rect pipBounds = new Rect(); + pipBounds.set(mMotionHelper.getBounds()); + mTmpBounds.offsetTo(newX, newY); + mMotionHelper.movePip(mTmpBounds); + result = true; + break; + case AccessibilityNodeInfo.ACTION_EXPAND: + mMotionHelper.expandLeavePip(); + result = true; + break; + default: + // Leave result as false + } + } + } + try { + callback.setPerformAccessibilityActionResult(result, interactionId); + } catch (RemoteException re) { + /* best effort - ignore */ + } + } + + private void setToExpandedBounds() { + float savedSnapFraction = mSnapAlgorithm.getSnapFraction( + mPipBoundsState.getBounds(), mNormalMovementBounds); + mSnapAlgorithm.applySnapFraction(mExpandedBounds, mExpandedMovementBounds, + savedSnapFraction); + mTaskOrganizer.scheduleFinishResizePip(mExpandedBounds, (Rect bounds) -> { + mMotionHelper.synchronizePinnedStackBounds(); + mUpdateMovementBoundCallback.run(); + }); + } + + private void setToNormalBounds() { + float savedSnapFraction = mSnapAlgorithm.getSnapFraction( + mPipBoundsState.getBounds(), mExpandedMovementBounds); + mSnapAlgorithm.applySnapFraction(mNormalBounds, mNormalMovementBounds, savedSnapFraction); + mTaskOrganizer.scheduleFinishResizePip(mNormalBounds, (Rect bounds) -> { + mMotionHelper.synchronizePinnedStackBounds(); + mUpdateMovementBoundCallback.run(); + }); + } + + @Override + public void findAccessibilityNodeInfosByViewId(long accessibilityNodeId, + String viewId, Region interactiveRegion, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { + // We have no view with a proper ID + try { + callback.setFindAccessibilityNodeInfoResult(null, interactionId); + } catch (RemoteException re) { + /* best effort - ignore */ + } + } + + @Override + public void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, + Region interactiveRegion, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { + // We have no view with text + try { + callback.setFindAccessibilityNodeInfoResult(null, interactionId); + } catch (RemoteException re) { + /* best effort - ignore */ + } + } + + @Override + public void findFocus(long accessibilityNodeId, int focusType, Region interactiveRegion, + int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { + // We have no view that can take focus + try { + callback.setFindAccessibilityNodeInfoResult(null, interactionId); + } catch (RemoteException re) { + /* best effort - ignore */ + } + } + + @Override + public void focusSearch(long accessibilityNodeId, int direction, Region interactiveRegion, + int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { + // We have no view that can take focus + try { + callback.setFindAccessibilityNodeInfoResult(null, interactionId); + } catch (RemoteException re) { + /* best effort - ignore */ + } + } + + @Override + public void clearAccessibilityFocus() { + // We should not be here. + } + + @Override + public void notifyOutsideTouch() { + // Do nothing. + } + + /** + * Update the normal and expanded bounds so they can be used for Resize. + */ + void onMovementBoundsChanged(Rect normalBounds, Rect expandedBounds, Rect normalMovementBounds, + Rect expandedMovementBounds) { + mNormalBounds.set(normalBounds); + mExpandedBounds.set(expandedBounds); + mNormalMovementBounds.set(normalMovementBounds); + mExpandedMovementBounds.set(expandedMovementBounds); + } + + /** + * Update the Root node with PIP Accessibility action items. + */ + public static AccessibilityNodeInfo obtainRootAccessibilityNodeInfo(Context context) { + AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); + info.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID, + AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_MOVE_WINDOW); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND); + info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_pip_resize, + context.getString(R.string.accessibility_action_pip_resize))); + info.setImportantForAccessibility(true); + info.setClickable(true); + info.setVisibleToUser(true); + return info; + } + + private List<AccessibilityNodeInfo> getNodeList() { + if (mAccessibilityNodeInfoList == null) { + mAccessibilityNodeInfoList = new ArrayList<>(1); + } + AccessibilityNodeInfo info = obtainRootAccessibilityNodeInfo(mContext); + mAccessibilityNodeInfoList.clear(); + mAccessibilityNodeInfoList.add(info); + return mAccessibilityNodeInfoList; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAppOpsListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAppOpsListener.java new file mode 100644 index 000000000000..6b6b5211b10a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAppOpsListener.java @@ -0,0 +1,97 @@ +/* + * 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.pip.phone; + +import static android.app.AppOpsManager.MODE_ALLOWED; +import static android.app.AppOpsManager.OP_PICTURE_IN_PICTURE; + +import android.app.AppOpsManager; +import android.app.AppOpsManager.OnOpChangedListener; +import android.app.IActivityManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Handler; +import android.util.Pair; + +public class PipAppOpsListener { + private static final String TAG = PipAppOpsListener.class.getSimpleName(); + + private Context mContext; + private Handler mHandler; + private IActivityManager mActivityManager; + private AppOpsManager mAppOpsManager; + private Callback mCallback; + + private AppOpsManager.OnOpChangedListener mAppOpsChangedListener = new OnOpChangedListener() { + @Override + public void onOpChanged(String op, String packageName) { + try { + // Dismiss the PiP once the user disables the app ops setting for that package + final Pair<ComponentName, Integer> topPipActivityInfo = + PipUtils.getTopPipActivity(mContext, mActivityManager); + if (topPipActivityInfo.first != null) { + final ApplicationInfo appInfo = mContext.getPackageManager() + .getApplicationInfoAsUser(packageName, 0, topPipActivityInfo.second); + if (appInfo.packageName.equals(topPipActivityInfo.first.getPackageName()) && + mAppOpsManager.checkOpNoThrow(OP_PICTURE_IN_PICTURE, appInfo.uid, + packageName) != MODE_ALLOWED) { + mHandler.post(() -> mCallback.dismissPip()); + } + } + } catch (NameNotFoundException e) { + // Unregister the listener if the package can't be found + unregisterAppOpsListener(); + } + } + }; + + public PipAppOpsListener(Context context, IActivityManager activityManager, + Callback callback) { + mContext = context; + mHandler = new Handler(mContext.getMainLooper()); + mActivityManager = activityManager; + mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); + mCallback = callback; + } + + public void onActivityPinned(String packageName) { + // Register for changes to the app ops setting for this package while it is in PiP + registerAppOpsListener(packageName); + } + + public void onActivityUnpinned() { + // Unregister for changes to the previously PiP'ed package + unregisterAppOpsListener(); + } + + private void registerAppOpsListener(String packageName) { + mAppOpsManager.startWatchingMode(OP_PICTURE_IN_PICTURE, packageName, + mAppOpsChangedListener); + } + + private void unregisterAppOpsListener() { + mAppOpsManager.stopWatchingMode(mAppOpsChangedListener); + } + + /** Callback for PipAppOpsListener to request changes to the PIP window. */ + public interface Callback { + /** Dismisses the PIP window. */ + void dismissPip(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java new file mode 100644 index 000000000000..edc68e5221f1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -0,0 +1,486 @@ +/* + * 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.pip.phone; + +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; + +import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; + +import android.app.ActivityManager; +import android.app.PictureInPictureParams; +import android.app.RemoteAction; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.ParceledListSlice; +import android.graphics.Rect; +import android.os.Handler; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Slog; +import android.view.DisplayInfo; +import android.view.IPinnedStackController; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.wm.shell.WindowManagerShellWrapper; +import com.android.wm.shell.common.DisplayChangeController; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.pip.PinnedStackListenerForwarder; +import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.pip.PipBoundsHandler; +import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipTaskOrganizer; + +import java.io.PrintWriter; +import java.util.function.Consumer; + +/** + * Manages the picture-in-picture (PIP) UI and states for Phones. + */ +public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallback { + private static final String TAG = "PipController"; + + private Context mContext; + private Handler mHandler = new Handler(); + + private final DisplayInfo mTmpDisplayInfo = new DisplayInfo(); + private final Rect mTmpInsetBounds = new Rect(); + private final Rect mTmpNormalBounds = new Rect(); + protected final Rect mReentryBounds = new Rect(); + + private DisplayController mDisplayController; + private PipAppOpsListener mAppOpsListener; + private PipBoundsHandler mPipBoundsHandler; + private @NonNull PipBoundsState mPipBoundsState; + private PipMediaController mMediaController; + private PipTouchHandler mTouchHandler; + private Consumer<Boolean> mPinnedStackAnimationRecentsCallback; + private WindowManagerShellWrapper mWindowManagerShellWrapper; + + private boolean mIsInFixedRotation; + + protected PipMenuActivityController mMenuController; + protected PipTaskOrganizer mPipTaskOrganizer; + + /** + * Handler for display rotation changes. + */ + private final DisplayChangeController.OnDisplayChangingListener mRotationController = ( + int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) -> { + if (!mPipTaskOrganizer.isInPip() || mPipTaskOrganizer.isDeferringEnterPipAnimation()) { + // Skip if we aren't in PIP or haven't actually entered PIP yet. We still need to update + // the display layout in the bounds handler in this case. + mPipBoundsHandler.onDisplayRotationChangedNotInPip(mContext, toRotation); + return; + } + // If there is an animation running (ie. from a shelf offset), then ensure that we calculate + // the bounds for the next orientation using the destination bounds of the animation + // TODO: Technically this should account for movement animation bounds as well + Rect currentBounds = mPipTaskOrganizer.getCurrentOrAnimatingBounds(); + final boolean changed = mPipBoundsHandler.onDisplayRotationChanged(mContext, + mTmpNormalBounds, currentBounds, mTmpInsetBounds, displayId, fromRotation, + toRotation, t); + if (changed) { + // If the pip was in the offset zone earlier, adjust the new bounds to the bottom of the + // movement bounds + mTouchHandler.adjustBoundsForRotation(mTmpNormalBounds, + mPipBoundsState.getBounds(), mTmpInsetBounds); + + // The bounds are being applied to a specific snap fraction, so reset any known offsets + // for the previous orientation before updating the movement bounds. + // We perform the resets if and only if this callback is due to screen rotation but + // not during the fixed rotation. In fixed rotation case, app is about to enter PiP + // and we need the offsets preserved to calculate the destination bounds. + if (!mIsInFixedRotation) { + mPipBoundsHandler.setShelfHeight(false, 0); + mPipBoundsHandler.onImeVisibilityChanged(false, 0); + mTouchHandler.onShelfVisibilityChanged(false, 0); + mTouchHandler.onImeVisibilityChanged(false, 0); + } + + updateMovementBounds(mTmpNormalBounds, true /* fromRotation */, + false /* fromImeAdjustment */, false /* fromShelfAdjustment */, t); + } + }; + + private DisplayController.OnDisplaysChangedListener mFixedRotationListener = + new DisplayController.OnDisplaysChangedListener() { + @Override + public void onFixedRotationStarted(int displayId, int newRotation) { + mIsInFixedRotation = true; + } + + @Override + public void onFixedRotationFinished(int displayId) { + mIsInFixedRotation = false; + } + + @Override + public void onDisplayAdded(int displayId) { + mPipBoundsHandler.setDisplayLayout( + mDisplayController.getDisplayLayout(displayId)); + } + }; + + /** + * Handler for messages from the PIP controller. + */ + private class PipControllerPinnedStackListener extends + PinnedStackListenerForwarder.PinnedStackListener { + @Override + public void onListenerRegistered(IPinnedStackController controller) { + mHandler.post(() -> mTouchHandler.setPinnedStackController(controller)); + } + + @Override + public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { + mHandler.post(() -> { + mPipBoundsHandler.onImeVisibilityChanged(imeVisible, imeHeight); + mTouchHandler.onImeVisibilityChanged(imeVisible, imeHeight); + }); + } + + @Override + public void onMovementBoundsChanged(boolean fromImeAdjustment) { + mHandler.post(() -> updateMovementBounds(null /* toBounds */, + false /* fromRotation */, fromImeAdjustment, false /* fromShelfAdjustment */, + null /* windowContainerTransaction */)); + } + + @Override + public void onActionsChanged(ParceledListSlice<RemoteAction> actions) { + mHandler.post(() -> mMenuController.setAppActions(actions)); + } + + @Override + public void onActivityHidden(ComponentName componentName) { + mHandler.post(() -> { + if (componentName.equals(mPipBoundsState.getLastPipComponentName())) { + // The activity was removed, we don't want to restore to the reentry state + // saved for this component anymore. + mPipBoundsState.setLastPipComponentName(null); + } + }); + } + + @Override + public void onDisplayInfoChanged(DisplayInfo displayInfo) { + mHandler.post(() -> mPipBoundsHandler.onDisplayInfoChanged(displayInfo)); + } + + @Override + public void onConfigurationChanged() { + mHandler.post(() -> { + mPipBoundsHandler.onConfigurationChanged(mContext); + mTouchHandler.onConfigurationChanged(); + }); + } + + @Override + public void onAspectRatioChanged(float aspectRatio) { + // TODO(b/169373982): Remove this callback as it is redundant with PipTaskOrg params + // change. + mHandler.post(() -> { + mPipBoundsState.setAspectRatio(aspectRatio); + mTouchHandler.onAspectRatioChanged(); + }); + } + } + + protected PipController(Context context, + DisplayController displayController, + PipAppOpsListener pipAppOpsListener, + PipBoundsHandler pipBoundsHandler, + @NonNull PipBoundsState pipBoundsState, + PipMediaController pipMediaController, + PipMenuActivityController pipMenuActivityController, + PipTaskOrganizer pipTaskOrganizer, + PipTouchHandler pipTouchHandler, + WindowManagerShellWrapper windowManagerShellWrapper + ) { + // Ensure that we are the primary user's SystemUI. + final int processUser = UserManager.get(context).getUserHandle(); + if (processUser != UserHandle.USER_SYSTEM) { + throw new IllegalStateException("Non-primary Pip component not currently supported."); + } + + mContext = context; + mWindowManagerShellWrapper = windowManagerShellWrapper; + mDisplayController = displayController; + mPipBoundsHandler = pipBoundsHandler; + mPipBoundsState = pipBoundsState; + mPipTaskOrganizer = pipTaskOrganizer; + mPipTaskOrganizer.registerPipTransitionCallback(this); + mPipTaskOrganizer.registerOnDisplayIdChangeCallback((int displayId) -> { + final DisplayInfo newDisplayInfo = new DisplayInfo(); + displayController.getDisplay(displayId).getDisplayInfo(newDisplayInfo); + mPipBoundsHandler.onDisplayInfoChanged(newDisplayInfo); + updateMovementBounds(null /* toBounds */, false /* fromRotation */, + false /* fromImeAdjustment */, false /* fromShelfAdustment */, + null /* wct */); + }); + mMediaController = pipMediaController; + mMenuController = pipMenuActivityController; + mTouchHandler = pipTouchHandler; + mAppOpsListener = pipAppOpsListener; + displayController.addDisplayChangingController(mRotationController); + displayController.addDisplayWindowListener(mFixedRotationListener); + + // Ensure that we have the display info in case we get calls to update the bounds before the + // listener calls back + final DisplayInfo displayInfo = new DisplayInfo(); + context.getDisplay().getDisplayInfo(displayInfo); + mPipBoundsHandler.onDisplayInfoChanged(displayInfo); + + try { + mWindowManagerShellWrapper.addPinnedStackListener( + new PipControllerPinnedStackListener()); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to register pinned stack listener", e); + } + } + + @Override + public void onDensityOrFontScaleChanged() { + mHandler.post(() -> { + mPipTaskOrganizer.onDensityOrFontScaleChanged(mContext); + }); + } + + @Override + public void onActivityPinned(String packageName) { + mHandler.post(() -> { + mTouchHandler.onActivityPinned(); + mMediaController.onActivityPinned(); + mMenuController.onActivityPinned(); + mAppOpsListener.onActivityPinned(packageName); + }); + } + + @Override + public void onActivityUnpinned(ComponentName topActivity) { + mHandler.post(() -> { + mMenuController.onActivityUnpinned(); + mTouchHandler.onActivityUnpinned(topActivity); + mAppOpsListener.onActivityUnpinned(); + }); + } + + @Override + public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, + boolean clearedTask) { + if (task.configuration.windowConfiguration.getWindowingMode() + != WINDOWING_MODE_PINNED) { + return; + } + mTouchHandler.getMotionHelper().expandLeavePip(clearedTask /* skipAnimation */); + } + + @Override + public void onOverlayChanged() { + mHandler.post(() -> { + mPipBoundsHandler.onOverlayChanged(mContext, mContext.getDisplay()); + updateMovementBounds(null /* toBounds */, + false /* fromRotation */, false /* fromImeAdjustment */, + false /* fromShelfAdjustment */, + null /* windowContainerTransaction */); + }); + } + + @Override + public void registerSessionListenerForCurrentUser() { + mMediaController.registerSessionListenerForCurrentUser(); + } + + @Override + public void onSystemUiStateChanged(boolean isValidState, int flag) { + mTouchHandler.onSystemUiStateChanged(isValidState); + } + + /** + * Expands the PIP. + */ + @Override + public void expandPip() { + mTouchHandler.getMotionHelper().expandLeavePip(false /* skipAnimation */); + } + + @Override + public PipTouchHandler getPipTouchHandler() { + return mTouchHandler; + } + + /** + * Hides the PIP menu. + */ + @Override + public void hidePipMenu(Runnable onStartCallback, Runnable onEndCallback) { + mMenuController.hideMenu(onStartCallback, onEndCallback); + } + + /** + * Sent from KEYCODE_WINDOW handler in PhoneWindowManager, to request the menu to be shown. + */ + public void showPictureInPictureMenu() { + mTouchHandler.showPictureInPictureMenu(); + } + + /** + * Sets a customized touch gesture that replaces the default one. + */ + public void setTouchGesture(PipTouchGesture gesture) { + mTouchHandler.setTouchGesture(gesture); + } + + /** + * Sets both shelf visibility and its height. + */ + @Override + public void setShelfHeight(boolean visible, int height) { + mHandler.post(() -> setShelfHeightLocked(visible, height)); + } + + private void setShelfHeightLocked(boolean visible, int height) { + final int shelfHeight = visible ? height : 0; + final boolean changed = mPipBoundsHandler.setShelfHeight(visible, shelfHeight); + if (changed) { + mTouchHandler.onShelfVisibilityChanged(visible, shelfHeight); + updateMovementBounds(mPipBoundsState.getBounds(), + false /* fromRotation */, false /* fromImeAdjustment */, + true /* fromShelfAdjustment */, null /* windowContainerTransaction */); + } + } + + @Override + public void setPinnedStackAnimationType(int animationType) { + mHandler.post(() -> mPipTaskOrganizer.setOneShotAnimationType(animationType)); + } + + @Override + public void setPinnedStackAnimationListener(Consumer<Boolean> callback) { + mHandler.post(() -> mPinnedStackAnimationRecentsCallback = callback); + } + + @Override + public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, + PictureInPictureParams pictureInPictureParams, + int launcherRotation, int shelfHeight) { + setShelfHeightLocked(shelfHeight > 0 /* visible */, shelfHeight); + mPipBoundsHandler.onDisplayRotationChangedNotInPip(mContext, launcherRotation); + return mPipTaskOrganizer.startSwipePipToHome(componentName, activityInfo, + pictureInPictureParams); + } + + @Override + public void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds) { + mPipTaskOrganizer.stopSwipePipToHome(componentName, destinationBounds); + } + + @Override + public void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds) { + if (isOutPipDirection(direction)) { + // Exiting PIP, save the reentry bounds to restore to when re-entering. + updateReentryBounds(pipBounds); + final float snapFraction = mPipBoundsHandler.getSnapFraction(mReentryBounds); + mPipBoundsState.saveReentryState(mReentryBounds, snapFraction); + } + // Disable touches while the animation is running + mTouchHandler.setTouchEnabled(false); + if (mPinnedStackAnimationRecentsCallback != null) { + mPinnedStackAnimationRecentsCallback.accept(true); + } + } + + /** + * Update the bounds used to save the re-entry size and snap fraction when exiting PIP. + */ + public void updateReentryBounds(Rect bounds) { + final Rect reentryBounds = mTouchHandler.getUserResizeBounds(); + float snapFraction = mPipBoundsHandler.getSnapFraction(bounds); + mPipBoundsHandler.applySnapFraction(reentryBounds, snapFraction); + mReentryBounds.set(reentryBounds); + } + + @Override + public void onPipTransitionFinished(ComponentName activity, int direction) { + onPipTransitionFinishedOrCanceled(direction); + } + + @Override + public void onPipTransitionCanceled(ComponentName activity, int direction) { + onPipTransitionFinishedOrCanceled(direction); + } + + private void onPipTransitionFinishedOrCanceled(int direction) { + // Re-enable touches after the animation completes + mTouchHandler.setTouchEnabled(true); + mTouchHandler.onPinnedStackAnimationEnded(direction); + mMenuController.onPinnedStackAnimationEnded(); + } + + private void updateMovementBounds(@Nullable Rect toBounds, boolean fromRotation, + boolean fromImeAdjustment, boolean fromShelfAdjustment, + WindowContainerTransaction wct) { + // Populate inset / normal bounds and DisplayInfo from mPipBoundsHandler before + // passing to mTouchHandler/mPipTaskOrganizer + final Rect outBounds = new Rect(toBounds); + mPipBoundsHandler.onMovementBoundsChanged(mTmpInsetBounds, mTmpNormalBounds, + outBounds, mTmpDisplayInfo); + // mTouchHandler would rely on the bounds populated from mPipTaskOrganizer + mPipTaskOrganizer.onMovementBoundsChanged(outBounds, fromRotation, fromImeAdjustment, + fromShelfAdjustment, wct); + mTouchHandler.onMovementBoundsChanged(mTmpInsetBounds, mTmpNormalBounds, + outBounds, fromImeAdjustment, fromShelfAdjustment, + mTmpDisplayInfo.rotation); + } + + @Override + public void dump(PrintWriter pw) { + final String innerPrefix = " "; + pw.println(TAG); + mMenuController.dump(pw, innerPrefix); + mTouchHandler.dump(pw, innerPrefix); + mPipBoundsHandler.dump(pw, innerPrefix); + mPipTaskOrganizer.dump(pw, innerPrefix); + mPipBoundsState.dump(pw, innerPrefix); + } + + /** + * Instantiates {@link PipController}, returns {@code null} if the feature not supported. + */ + @Nullable + public static PipController create(Context context, DisplayController displayController, + PipAppOpsListener pipAppOpsListener, PipBoundsHandler pipBoundsHandler, + PipBoundsState pipBoundsState, PipMediaController pipMediaController, + PipMenuActivityController pipMenuActivityController, + PipTaskOrganizer pipTaskOrganizer, PipTouchHandler pipTouchHandler, + WindowManagerShellWrapper windowManagerShellWrapper) { + if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { + Slog.w(TAG, "Device doesn't support Pip feature"); + return null; + } + + return new PipController(context, displayController, pipAppOpsListener, pipBoundsHandler, + pipBoundsState, pipMediaController, pipMenuActivityController, + pipTaskOrganizer, pipTouchHandler, windowManagerShellWrapper); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java new file mode 100644 index 000000000000..bebe5f965251 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java @@ -0,0 +1,295 @@ +/* + * 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.pip.phone; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.TransitionDrawable; +import android.os.Handler; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.dynamicanimation.animation.SpringForce; + +import com.android.wm.shell.R; +import com.android.wm.shell.animation.PhysicsAnimator; +import com.android.wm.shell.common.DismissCircleView; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; +import com.android.wm.shell.pip.PipUiEventLogger; + +import kotlin.Unit; + +/** + * Handler of all Magnetized Object related code for PiP. + */ +public class PipDismissTargetHandler { + + /* The multiplier to apply scale the target size by when applying the magnetic field radius */ + private static final float MAGNETIC_FIELD_RADIUS_MULTIPLIER = 1.25f; + + /** Duration of the dismiss scrim fading in/out. */ + private static final int DISMISS_TRANSITION_DURATION_MS = 200; + + /** + * MagnetizedObject wrapper for PIP. This allows the magnetic target library to locate and move + * PIP. + */ + private final MagnetizedObject<Rect> mMagnetizedPip; + + /** + * Container for the dismiss circle, so that it can be animated within the container via + * translation rather than within the WindowManager via slow layout animations. + */ + private final ViewGroup mTargetViewContainer; + + /** Circle view used to render the dismiss target. */ + private final DismissCircleView mTargetView; + + /** + * MagneticTarget instance wrapping the target view and allowing us to set its magnetic radius. + */ + private final MagnetizedObject.MagneticTarget mMagneticTarget; + + /** PhysicsAnimator instance for animating the dismiss target in/out. */ + private final PhysicsAnimator<View> mMagneticTargetAnimator; + + /** Default configuration to use for springing the dismiss target in/out. */ + private final PhysicsAnimator.SpringConfig mTargetSpringConfig = + new PhysicsAnimator.SpringConfig( + SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY); + + /** + * Runnable that can be posted delayed to show the target. This needs to be saved as a member + * variable so we can pass it to removeCallbacks. + */ + private Runnable mShowTargetAction = this::showDismissTargetMaybe; + + // Allow dragging the PIP to a location to close it + private final boolean mEnableDismissDragToEdge; + + private int mDismissAreaHeight; + + private final Context mContext; + private final PipMotionHelper mMotionHelper; + private final PipUiEventLogger mPipUiEventLogger; + private final WindowManager mWindowManager; + private final Handler mHandler; + + public PipDismissTargetHandler(Context context, PipUiEventLogger pipUiEventLogger, + PipMotionHelper motionHelper, Handler handler) { + mContext = context; + mPipUiEventLogger = pipUiEventLogger; + mMotionHelper = motionHelper; + mHandler = handler; + mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + + Resources res = context.getResources(); + mEnableDismissDragToEdge = res.getBoolean(R.bool.config_pipEnableDismissDragToEdge); + mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height); + + mTargetView = new DismissCircleView(context); + mTargetViewContainer = new FrameLayout(context); + mTargetViewContainer.setBackgroundDrawable( + context.getDrawable(R.drawable.floating_dismiss_gradient_transition)); + mTargetViewContainer.setClipChildren(false); + mTargetViewContainer.addView(mTargetView); + + mMagnetizedPip = mMotionHelper.getMagnetizedPip(); + mMagneticTarget = mMagnetizedPip.addTarget(mTargetView, 0); + updateMagneticTargetSize(); + + mMagnetizedPip.setAnimateStuckToTarget( + (target, velX, velY, flung, after) -> { + if (mEnableDismissDragToEdge) { + mMotionHelper.animateIntoDismissTarget(target, velX, velY, flung, after); + } + return Unit.INSTANCE; + }); + mMagnetizedPip.setMagnetListener(new MagnetizedObject.MagnetListener() { + @Override + public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { + // Show the dismiss target, in case the initial touch event occurred within the + // magnetic field radius. + if (mEnableDismissDragToEdge) { + showDismissTargetMaybe(); + } + } + + @Override + public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, + float velX, float velY, boolean wasFlungOut) { + if (wasFlungOut) { + mMotionHelper.flingToSnapTarget(velX, velY, null /* endAction */); + hideDismissTargetMaybe(); + } else { + mMotionHelper.setSpringingToTouch(true); + } + } + + @Override + public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { + mMotionHelper.notifyDismissalPending(); + + handler.post(() -> { + mMotionHelper.animateDismiss(); + hideDismissTargetMaybe(); + }); + + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_DRAG_TO_REMOVE); + } + }); + + mMagneticTargetAnimator = PhysicsAnimator.getInstance(mTargetView); + } + + /** + * Potentially start consuming future motion events if PiP is currently near the magnetized + * object. + */ + public boolean maybeConsumeMotionEvent(MotionEvent ev) { + return mMagnetizedPip.maybeConsumeMotionEvent(ev); + } + + /** + * Update the magnet size. + */ + public void updateMagneticTargetSize() { + if (mTargetView == null) { + return; + } + + final Resources res = mContext.getResources(); + final int targetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size); + mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height); + final FrameLayout.LayoutParams newParams = + new FrameLayout.LayoutParams(targetSize, targetSize); + newParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; + newParams.bottomMargin = mContext.getResources().getDimensionPixelSize( + R.dimen.floating_dismiss_bottom_margin); + mTargetView.setLayoutParams(newParams); + + // Set the magnetic field radius equal to the target size from the center of the target + mMagneticTarget.setMagneticFieldRadiusPx( + (int) (targetSize * MAGNETIC_FIELD_RADIUS_MULTIPLIER)); + } + + /** Adds the magnetic target view to the WindowManager so it's ready to be animated in. */ + public void createOrUpdateDismissTarget() { + if (!mTargetViewContainer.isAttachedToWindow()) { + mHandler.removeCallbacks(mShowTargetAction); + mMagneticTargetAnimator.cancel(); + + mTargetViewContainer.setVisibility(View.INVISIBLE); + + try { + mWindowManager.addView(mTargetViewContainer, getDismissTargetLayoutParams()); + } catch (IllegalStateException e) { + // This shouldn't happen, but if the target is already added, just update its layout + // params. + mWindowManager.updateViewLayout( + mTargetViewContainer, getDismissTargetLayoutParams()); + } + } else { + mWindowManager.updateViewLayout(mTargetViewContainer, getDismissTargetLayoutParams()); + } + } + + /** Returns layout params for the dismiss target, using the latest display metrics. */ + private WindowManager.LayoutParams getDismissTargetLayoutParams() { + final Point windowSize = new Point(); + mWindowManager.getDefaultDisplay().getRealSize(windowSize); + + final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + WindowManager.LayoutParams.MATCH_PARENT, + mDismissAreaHeight, + 0, windowSize.y - mDismissAreaHeight, + WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT); + + lp.setTitle("pip-dismiss-overlay"); + lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + lp.setFitInsetsTypes(0 /* types */); + + return lp; + } + + /** Makes the dismiss target visible and animates it in, if it isn't already visible. */ + public void showDismissTargetMaybe() { + if (!mEnableDismissDragToEdge) { + return; + } + + createOrUpdateDismissTarget(); + + if (mTargetViewContainer.getVisibility() != View.VISIBLE) { + + mTargetView.setTranslationY(mTargetViewContainer.getHeight()); + mTargetViewContainer.setVisibility(View.VISIBLE); + + // Cancel in case we were in the middle of animating it out. + mMagneticTargetAnimator.cancel(); + mMagneticTargetAnimator + .spring(DynamicAnimation.TRANSLATION_Y, 0f, mTargetSpringConfig) + .start(); + + ((TransitionDrawable) mTargetViewContainer.getBackground()).startTransition( + DISMISS_TRANSITION_DURATION_MS); + } + } + + /** Animates the magnetic dismiss target out and then sets it to GONE. */ + public void hideDismissTargetMaybe() { + if (!mEnableDismissDragToEdge) { + return; + } + + mHandler.removeCallbacks(mShowTargetAction); + mMagneticTargetAnimator + .spring(DynamicAnimation.TRANSLATION_Y, + mTargetViewContainer.getHeight(), + mTargetSpringConfig) + .withEndActions(() -> mTargetViewContainer.setVisibility(View.GONE)) + .start(); + + ((TransitionDrawable) mTargetViewContainer.getBackground()).reverseTransition( + DISMISS_TRANSITION_DURATION_MS); + } + + /** + * Removes the dismiss target and cancels any pending callbacks to show it. + */ + public void cleanUpDismissTarget() { + mHandler.removeCallbacks(mShowTargetAction); + + if (mTargetViewContainer.isAttachedToWindow()) { + mWindowManager.removeViewImmediate(mTargetViewContainer); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMediaController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMediaController.java new file mode 100644 index 000000000000..64e3758fd81a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMediaController.java @@ -0,0 +1,261 @@ +/* + * 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.pip.phone; + +import static android.app.PendingIntent.FLAG_IMMUTABLE; +import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; + +import android.app.IActivityManager; +import android.app.PendingIntent; +import android.app.RemoteAction; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.drawable.Icon; +import android.media.session.MediaController; +import android.media.session.MediaSessionManager; +import android.media.session.PlaybackState; +import android.os.UserHandle; + +import com.android.wm.shell.R; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Interfaces with the {@link MediaSessionManager} to compose the right set of actions to show (only + * if there are no actions from the PiP activity itself). The active media controller is only set + * when there is a media session from the top PiP activity. + */ +public class PipMediaController { + + private static final String ACTION_PLAY = "com.android.wm.shell.pip.phone.PLAY"; + private static final String ACTION_PAUSE = "com.android.wm.shell.pip.phone.PAUSE"; + private static final String ACTION_NEXT = "com.android.wm.shell.pip.phone.NEXT"; + private static final String ACTION_PREV = "com.android.wm.shell.pip.phone.PREV"; + + /** + * A listener interface to receive notification on changes to the media actions. + */ + public interface ActionListener { + /** + * Called when the media actions changes. + */ + void onMediaActionsChanged(List<RemoteAction> actions); + } + + private final Context mContext; + private final IActivityManager mActivityManager; + + private final MediaSessionManager mMediaSessionManager; + private MediaController mMediaController; + + private RemoteAction mPauseAction; + private RemoteAction mPlayAction; + private RemoteAction mNextAction; + private RemoteAction mPrevAction; + + private BroadcastReceiver mPlayPauseActionReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (action.equals(ACTION_PLAY)) { + mMediaController.getTransportControls().play(); + } else if (action.equals(ACTION_PAUSE)) { + mMediaController.getTransportControls().pause(); + } else if (action.equals(ACTION_NEXT)) { + mMediaController.getTransportControls().skipToNext(); + } else if (action.equals(ACTION_PREV)) { + mMediaController.getTransportControls().skipToPrevious(); + } + } + }; + + private final MediaController.Callback mPlaybackChangedListener = + new MediaController.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackState state) { + notifyActionsChanged(); + } + }; + + private final MediaSessionManager.OnActiveSessionsChangedListener mSessionsChangedListener = + controllers -> resolveActiveMediaController(controllers); + + private ArrayList<ActionListener> mListeners = new ArrayList<>(); + + public PipMediaController(Context context, IActivityManager activityManager) { + mContext = context; + mActivityManager = activityManager; + IntentFilter mediaControlFilter = new IntentFilter(); + mediaControlFilter.addAction(ACTION_PLAY); + mediaControlFilter.addAction(ACTION_PAUSE); + mediaControlFilter.addAction(ACTION_NEXT); + mediaControlFilter.addAction(ACTION_PREV); + mContext.registerReceiver(mPlayPauseActionReceiver, mediaControlFilter, + UserHandle.USER_ALL); + + createMediaActions(); + mMediaSessionManager = + (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE); + } + + /** + * Handles when an activity is pinned. + */ + public void onActivityPinned() { + // Once we enter PiP, try to find the active media controller for the top most activity + resolveActiveMediaController(mMediaSessionManager.getActiveSessionsForUser(null, + UserHandle.USER_CURRENT)); + } + + /** + * Adds a new media action listener. + */ + public void addListener(ActionListener listener) { + if (!mListeners.contains(listener)) { + mListeners.add(listener); + listener.onMediaActionsChanged(getMediaActions()); + } + } + + /** + * Removes a media action listener. + */ + public void removeListener(ActionListener listener) { + listener.onMediaActionsChanged(Collections.EMPTY_LIST); + mListeners.remove(listener); + } + + /** + * Gets the set of media actions currently available. + */ + private List<RemoteAction> getMediaActions() { + if (mMediaController == null || mMediaController.getPlaybackState() == null) { + return Collections.EMPTY_LIST; + } + + ArrayList<RemoteAction> mediaActions = new ArrayList<>(); + boolean isPlaying = mMediaController.getPlaybackState().isActiveState(); + long actions = mMediaController.getPlaybackState().getActions(); + + // Prev action + mPrevAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0); + mediaActions.add(mPrevAction); + + // Play/pause action + if (!isPlaying && ((actions & PlaybackState.ACTION_PLAY) != 0)) { + mediaActions.add(mPlayAction); + } else if (isPlaying && ((actions & PlaybackState.ACTION_PAUSE) != 0)) { + mediaActions.add(mPauseAction); + } + + // Next action + mNextAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0); + mediaActions.add(mNextAction); + return mediaActions; + } + + /** + * Creates the standard media buttons that we may show. + */ + private void createMediaActions() { + String pauseDescription = mContext.getString(R.string.pip_pause); + mPauseAction = new RemoteAction(Icon.createWithResource(mContext, + R.drawable.pip_ic_pause_white), pauseDescription, pauseDescription, + PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_PAUSE), + FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE)); + + String playDescription = mContext.getString(R.string.pip_play); + mPlayAction = new RemoteAction(Icon.createWithResource(mContext, + R.drawable.pip_ic_play_arrow_white), playDescription, playDescription, + PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_PLAY), + FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE)); + + String nextDescription = mContext.getString(R.string.pip_skip_to_next); + mNextAction = new RemoteAction(Icon.createWithResource(mContext, + R.drawable.pip_ic_skip_next_white), nextDescription, nextDescription, + PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_NEXT), + FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE)); + + String prevDescription = mContext.getString(R.string.pip_skip_to_prev); + mPrevAction = new RemoteAction(Icon.createWithResource(mContext, + R.drawable.pip_ic_skip_previous_white), prevDescription, prevDescription, + PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_PREV), + FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE)); + } + + /** + * Re-registers the session listener for the current user. + */ + public void registerSessionListenerForCurrentUser() { + mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionsChangedListener); + mMediaSessionManager.addOnActiveSessionsChangedListener(mSessionsChangedListener, null, + UserHandle.USER_CURRENT, null); + } + + /** + * Tries to find and set the active media controller for the top PiP activity. + */ + private void resolveActiveMediaController(List<MediaController> controllers) { + if (controllers != null) { + final ComponentName topActivity = PipUtils.getTopPipActivity(mContext, + mActivityManager).first; + if (topActivity != null) { + for (int i = 0; i < controllers.size(); i++) { + final MediaController controller = controllers.get(i); + if (controller.getPackageName().equals(topActivity.getPackageName())) { + setActiveMediaController(controller); + return; + } + } + } + } + setActiveMediaController(null); + } + + /** + * Sets the active media controller for the top PiP activity. + */ + private void setActiveMediaController(MediaController controller) { + if (controller != mMediaController) { + if (mMediaController != null) { + mMediaController.unregisterCallback(mPlaybackChangedListener); + } + mMediaController = controller; + if (controller != null) { + controller.registerCallback(mPlaybackChangedListener); + } + notifyActionsChanged(); + + // TODO(winsonc): Consider if we want to close the PIP after a timeout (like on TV) + } + } + + /** + * Notifies all listeners that the actions have changed. + */ + private void notifyActionsChanged() { + if (!mListeners.isEmpty()) { + List<RemoteAction> actions = getMediaActions(); + mListeners.forEach(l -> l.onMediaActionsChanged(actions)); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuActivityController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuActivityController.java new file mode 100644 index 000000000000..cd47d55da7f0 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuActivityController.java @@ -0,0 +1,395 @@ +/* + * 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.pip.phone; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; + +import android.app.ActivityTaskManager; +import android.app.ActivityTaskManager.RootTaskInfo; +import android.app.RemoteAction; +import android.content.Context; +import android.content.pm.ParceledListSlice; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.os.Debug; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.view.MotionEvent; +import android.view.WindowManager; +import android.view.WindowManagerGlobal; + +import com.android.wm.shell.pip.PipTaskOrganizer; +import com.android.wm.shell.pip.phone.PipMediaController.ActionListener; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * Manages the PiP menu activity which can show menu options or a scrim. + * + * The current media session provides actions whenever there are no valid actions provided by the + * current PiP activity. Otherwise, those actions always take precedence. + */ +public class PipMenuActivityController { + + private static final String TAG = "PipMenuActController"; + private static final boolean DEBUG = false; + + public static final int MENU_STATE_NONE = 0; + public static final int MENU_STATE_CLOSE = 1; + public static final int MENU_STATE_FULL = 2; + + /** + * A listener interface to receive notification on changes in PIP. + */ + public interface Listener { + /** + * Called when the PIP menu visibility changes. + * + * @param menuState the current state of the menu + * @param resize whether or not to resize the PiP with the state change + */ + void onPipMenuStateChanged(int menuState, boolean resize, Runnable callback); + + /** + * Called when the PIP requested to be expanded. + */ + void onPipExpand(); + + /** + * Called when the PIP requested to be dismissed. + */ + void onPipDismiss(); + + /** + * Called when the PIP requested to show the menu. + */ + void onPipShowMenu(); + } + + private Context mContext; + private PipTaskOrganizer mPipTaskOrganizer; + private PipMediaController mMediaController; + + private ArrayList<Listener> mListeners = new ArrayList<>(); + private ParceledListSlice<RemoteAction> mAppActions; + private ParceledListSlice<RemoteAction> mMediaActions; + private int mMenuState; + + private PipMenuView mPipMenuView; + private IBinder mPipMenuInputToken; + + private ActionListener mMediaActionListener = new ActionListener() { + @Override + public void onMediaActionsChanged(List<RemoteAction> mediaActions) { + mMediaActions = new ParceledListSlice<>(mediaActions); + updateMenuActions(); + } + }; + + public PipMenuActivityController(Context context, + PipMediaController mediaController, PipTaskOrganizer pipTaskOrganizer) { + mContext = context; + mMediaController = mediaController; + mPipTaskOrganizer = pipTaskOrganizer; + } + + public boolean isMenuVisible() { + return mPipMenuView != null && mMenuState != MENU_STATE_NONE; + } + + public void onActivityPinned() { + attachPipMenuView(); + } + + public void onActivityUnpinned() { + hideMenu(); + mPipTaskOrganizer.detachPipMenuViewHost(); + mPipMenuView = null; + mPipMenuInputToken = null; + } + + public void onPinnedStackAnimationEnded() { + if (isMenuVisible()) { + mPipMenuView.onPipAnimationEnded(); + } + } + + private void attachPipMenuView() { + if (mPipMenuView == null) { + mPipMenuView = new PipMenuView(mContext, this); + + } + + // If we haven't gotten the input toekn, that means we haven't had a success attempt + // yet at attaching the PipMenuView + if (mPipMenuInputToken == null) { + mPipMenuInputToken = mPipTaskOrganizer.attachPipMenuViewHost(mPipMenuView, + getPipMenuLayoutParams(0, 0)); + } + } + + /** + * Adds a new menu activity listener. + */ + public void addListener(Listener listener) { + if (!mListeners.contains(listener)) { + mListeners.add(listener); + } + } + + /** + * Similar to {@link #showMenu(int, Rect, boolean, boolean, boolean)} but only show the menu + * upon PiP window transition is finished. + */ + public void showMenuWithDelay(int menuState, Rect stackBounds, boolean allowMenuTimeout, + boolean willResizeMenu, boolean showResizeHandle) { + // hide all visible controls including close button and etc. first, this is to ensure + // menu is totally invisible during the transition to eliminate unpleasant artifacts + fadeOutMenu(); + showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu, + true /* withDelay */, showResizeHandle); + } + + /** + * Shows the menu activity immediately. + */ + public void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout, + boolean willResizeMenu, boolean showResizeHandle) { + showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu, + false /* withDelay */, showResizeHandle); + } + + private void showMenuInternal(int menuState, Rect stackBounds, boolean allowMenuTimeout, + boolean willResizeMenu, boolean withDelay, boolean showResizeHandle) { + if (DEBUG) { + Log.d(TAG, "showMenu() state=" + menuState + + " isMenuVisible=" + isMenuVisible() + + " allowMenuTimeout=" + allowMenuTimeout + + " willResizeMenu=" + willResizeMenu + + " withDelay=" + withDelay + + " showResizeHandle=" + showResizeHandle + + " callers=\n" + Debug.getCallers(5, " ")); + } + + if (!mPipTaskOrganizer.isPipMenuViewHostAttached()) { + Log.d(TAG, "PipMenu has not been attached yet. Attaching now at showMenuInternal()."); + attachPipMenuView(); + } + + mPipMenuView.showMenu(menuState, stackBounds, allowMenuTimeout, willResizeMenu, withDelay, + showResizeHandle); + } + + /** + * Pokes the menu, indicating that the user is interacting with it. + */ + public void pokeMenu() { + final boolean isMenuVisible = isMenuVisible(); + if (DEBUG) { + Log.d(TAG, "pokeMenu() isMenuVisible=" + isMenuVisible); + } + if (isMenuVisible) { + mPipMenuView.pokeMenu(); + } + } + + private void fadeOutMenu() { + final boolean isMenuVisible = isMenuVisible(); + if (DEBUG) { + Log.d(TAG, "fadeOutMenu() isMenuVisible=" + isMenuVisible); + } + if (isMenuVisible) { + mPipMenuView.fadeOutMenu(); + } + } + + /** + * Hides the menu activity. + */ + public void hideMenu() { + final boolean isMenuVisible = isMenuVisible(); + if (DEBUG) { + Log.d(TAG, "hideMenu() state=" + mMenuState + + " isMenuVisible=" + isMenuVisible + + " callers=\n" + Debug.getCallers(5, " ")); + } + if (isMenuVisible) { + mPipMenuView.hideMenu(); + } + } + + /** + * Hides the menu activity. + */ + public void hideMenu(Runnable onStartCallback, Runnable onEndCallback) { + if (isMenuVisible()) { + // If the menu is visible in either the closed or full state, then hide the menu and + // trigger the animation trigger afterwards + if (onStartCallback != null) { + onStartCallback.run(); + } + mPipMenuView.hideMenu(onEndCallback); + } + } + + /** + * Preemptively mark the menu as invisible, used when we are directly manipulating the pinned + * stack and don't want to trigger a resize which can animate the stack in a conflicting way + * (ie. when manually expanding or dismissing). + */ + public void hideMenuWithoutResize() { + onMenuStateChanged(MENU_STATE_NONE, false /* resize */, null /* callback */); + } + + /** + * Sets the menu actions to the actions provided by the current PiP activity. + */ + public void setAppActions(ParceledListSlice<RemoteAction> appActions) { + mAppActions = appActions; + updateMenuActions(); + } + + void onPipExpand() { + mListeners.forEach(Listener::onPipExpand); + } + + void onPipDismiss() { + mListeners.forEach(Listener::onPipDismiss); + } + + void onPipShowMenu() { + mListeners.forEach(Listener::onPipShowMenu); + } + + /** + * @return the best set of actions to show in the PiP menu. + */ + private ParceledListSlice<RemoteAction> resolveMenuActions() { + if (isValidActions(mAppActions)) { + return mAppActions; + } + return mMediaActions; + } + + /** + * Returns a default LayoutParams for the PIP Menu. + * @param width the PIP stack width. + * @param height the PIP stack height. + */ + public static WindowManager.LayoutParams getPipMenuLayoutParams(int width, int height) { + return new WindowManager.LayoutParams(width, height, + WindowManager.LayoutParams.TYPE_APPLICATION, 0, PixelFormat.TRANSLUCENT); + } + + /** + * Updates the PiP menu with the best set of actions provided. + */ + private void updateMenuActions() { + if (isMenuVisible()) { + // Fetch the pinned stack bounds + Rect stackBounds = null; + try { + RootTaskInfo pinnedTaskInfo = ActivityTaskManager.getService().getRootTaskInfo( + WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); + if (pinnedTaskInfo != null) { + stackBounds = pinnedTaskInfo.bounds; + } + } catch (RemoteException e) { + Log.e(TAG, "Error showing PIP menu", e); + } + + mPipMenuView.setActions(stackBounds, resolveMenuActions().getList()); + } + } + + /** + * Returns whether the set of actions are valid. + */ + private static boolean isValidActions(ParceledListSlice<?> actions) { + return actions != null && actions.getList().size() > 0; + } + + /** + * Handles changes in menu visibility. + */ + void onMenuStateChanged(int menuState, boolean resize, Runnable callback) { + if (DEBUG) { + Log.d(TAG, "onMenuStateChanged() mMenuState=" + mMenuState + + " menuState=" + menuState + " resize=" + resize + + " callers=\n" + Debug.getCallers(5, " ")); + } + + if (menuState != mMenuState) { + mListeners.forEach(l -> l.onPipMenuStateChanged(menuState, resize, callback)); + if (menuState == MENU_STATE_FULL) { + // Once visible, start listening for media action changes. This call will trigger + // the menu actions to be updated again. + mMediaController.addListener(mMediaActionListener); + } else { + // Once hidden, stop listening for media action changes. This call will trigger + // the menu actions to be updated again. + mMediaController.removeListener(mMediaActionListener); + } + + try { + WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */, + mPipMenuInputToken, menuState != MENU_STATE_NONE /* grantFocus */); + } catch (RemoteException e) { + Log.e(TAG, "Unable to update focus as menu appears/disappears", e); + } + } + mMenuState = menuState; + } + + /** + * Handles a pointer event sent from pip input consumer. + */ + void handlePointerEvent(MotionEvent ev) { + if (ev.isTouchEvent()) { + mPipMenuView.dispatchTouchEvent(ev); + } else { + mPipMenuView.dispatchGenericMotionEvent(ev); + } + } + + /** + * Tell the PIP Menu to recalculate its layout given its current position on the display. + */ + public void updateMenuLayout(Rect bounds) { + final boolean isMenuVisible = isMenuVisible(); + if (DEBUG) { + Log.d(TAG, "updateMenuLayout() state=" + mMenuState + + " isMenuVisible=" + isMenuVisible + + " callers=\n" + Debug.getCallers(5, " ")); + } + if (isMenuVisible) { + mPipMenuView.updateMenuLayout(bounds); + } + } + + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mMenuState=" + mMenuState); + pw.println(innerPrefix + "mPipMenuView=" + mPipMenuView); + pw.println(innerPrefix + "mListeners=" + mListeners.size()); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuIconsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuIconsAlgorithm.java new file mode 100644 index 000000000000..985cd0f1fa19 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuIconsAlgorithm.java @@ -0,0 +1,86 @@ +/* + * 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.pip.phone; + +import android.content.Context; +import android.graphics.Rect; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +/** + * Helper class to calculate and place the menu icons on the PIP Menu. + */ +public class PipMenuIconsAlgorithm { + + private static final String TAG = "PipMenuIconsAlgorithm"; + + private boolean mFinishedLayout = false; + protected ViewGroup mViewRoot; + protected ViewGroup mTopEndContainer; + protected View mDragHandle; + protected View mSettingsButton; + protected View mDismissButton; + + protected PipMenuIconsAlgorithm(Context context) { + } + + /** + * Bind the necessary views. + */ + public void bindViews(ViewGroup viewRoot, ViewGroup topEndContainer, View dragHandle, + View settingsButton, View dismissButton) { + mViewRoot = viewRoot; + mTopEndContainer = topEndContainer; + mDragHandle = dragHandle; + mSettingsButton = settingsButton; + mDismissButton = dismissButton; + } + + /** + * Updates the position of the drag handle based on where the PIP window is on the screen. + */ + public void onBoundsChanged(Rect bounds) { + if (mViewRoot == null || mTopEndContainer == null || mDragHandle == null + || mSettingsButton == null || mDismissButton == null) { + Log.e(TAG, "One if the required views is null."); + } + + //We only need to calculate the layout once since it does not change. + if (!mFinishedLayout) { + mTopEndContainer.removeView(mSettingsButton); + mViewRoot.addView(mSettingsButton); + + setLayoutGravity(mDragHandle, Gravity.START | Gravity.TOP); + setLayoutGravity(mSettingsButton, Gravity.START | Gravity.TOP); + mFinishedLayout = true; + } + } + + /** + * Set the gravity on the given view. + */ + protected static void setLayoutGravity(View v, int gravity) { + if (v.getLayoutParams() instanceof FrameLayout.LayoutParams) { + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) v.getLayoutParams(); + params.gravity = gravity; + v.setLayoutParams(params); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java new file mode 100644 index 000000000000..51951409f76c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java @@ -0,0 +1,474 @@ +/* + * 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.pip.phone; + +import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static android.provider.Settings.ACTION_PICTURE_IN_PICTURE_SETTINGS; +import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS; +import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS; +import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; + +import static com.android.wm.shell.pip.phone.PipMenuActivityController.MENU_STATE_CLOSE; +import static com.android.wm.shell.pip.phone.PipMenuActivityController.MENU_STATE_FULL; +import static com.android.wm.shell.pip.phone.PipMenuActivityController.MENU_STATE_NONE; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.app.ActivityManager; +import android.app.PendingIntent.CanceledException; +import android.app.RemoteAction; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.UserHandle; +import android.util.Log; +import android.util.Pair; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.LinearLayout; + +import com.android.wm.shell.R; +import com.android.wm.shell.animation.Interpolators; + +import java.util.ArrayList; +import java.util.List; + +/** + * Translucent window that gets started on top of a task in PIP to allow the user to control it. + */ +public class PipMenuView extends FrameLayout { + + private static final String TAG = "PipMenuView"; + + private static final int MESSAGE_INVALID_TYPE = -1; + public static final int MESSAGE_MENU_EXPANDED = 8; + + private static final int INITIAL_DISMISS_DELAY = 3500; + private static final int POST_INTERACTION_DISMISS_DELAY = 2000; + private static final long MENU_FADE_DURATION = 125; + private static final long MENU_SLOW_FADE_DURATION = 175; + private static final long MENU_SHOW_ON_EXPAND_START_DELAY = 30; + + private static final float MENU_BACKGROUND_ALPHA = 0.3f; + private static final float DISMISS_BACKGROUND_ALPHA = 0.6f; + + private static final float DISABLED_ACTION_ALPHA = 0.54f; + + private static final boolean ENABLE_RESIZE_HANDLE = false; + + private int mMenuState; + private boolean mResize = true; + private boolean mAllowMenuTimeout = true; + private boolean mAllowTouches = true; + + private final List<RemoteAction> mActions = new ArrayList<>(); + + private AccessibilityManager mAccessibilityManager; + private Drawable mBackgroundDrawable; + private View mMenuContainer; + private LinearLayout mActionsGroup; + private int mBetweenActionPaddingLand; + + private AnimatorSet mMenuContainerAnimator; + private PipMenuActivityController mController; + + private ValueAnimator.AnimatorUpdateListener mMenuBgUpdateListener = + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + final float alpha = (float) animation.getAnimatedValue(); + mBackgroundDrawable.setAlpha((int) (MENU_BACKGROUND_ALPHA * alpha * 255)); + } + }; + + private Handler mHandler = new Handler(); + + private final Runnable mHideMenuRunnable = this::hideMenu; + + protected View mViewRoot; + protected View mSettingsButton; + protected View mDismissButton; + protected View mResizeHandle; + protected View mTopEndContainer; + protected PipMenuIconsAlgorithm mPipMenuIconsAlgorithm; + + public PipMenuView(Context context, PipMenuActivityController controller) { + super(context, null, 0); + mContext = context; + mController = controller; + + mAccessibilityManager = context.getSystemService(AccessibilityManager.class); + inflate(context, R.layout.pip_menu, this); + + mBackgroundDrawable = new ColorDrawable(Color.BLACK); + mBackgroundDrawable.setAlpha(0); + mViewRoot = findViewById(R.id.background); + mViewRoot.setBackground(mBackgroundDrawable); + mMenuContainer = findViewById(R.id.menu_container); + mMenuContainer.setAlpha(0); + mTopEndContainer = findViewById(R.id.top_end_container); + mSettingsButton = findViewById(R.id.settings); + mSettingsButton.setAlpha(0); + mSettingsButton.setOnClickListener((v) -> { + if (v.getAlpha() != 0) { + showSettings(); + } + }); + mDismissButton = findViewById(R.id.dismiss); + mDismissButton.setAlpha(0); + mDismissButton.setOnClickListener(v -> dismissPip()); + findViewById(R.id.expand_button).setOnClickListener(v -> { + if (mMenuContainer.getAlpha() != 0) { + expandPip(); + } + }); + + mResizeHandle = findViewById(R.id.resize_handle); + mResizeHandle.setAlpha(0); + mActionsGroup = findViewById(R.id.actions_group); + mBetweenActionPaddingLand = getResources().getDimensionPixelSize( + R.dimen.pip_between_action_padding_land); + mPipMenuIconsAlgorithm = new PipMenuIconsAlgorithm(mContext); + mPipMenuIconsAlgorithm.bindViews((ViewGroup) mViewRoot, (ViewGroup) mTopEndContainer, + mResizeHandle, mSettingsButton, mDismissButton); + + initAccessibility(); + } + + private void initAccessibility() { + this.setAccessibilityDelegate(new View.AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + String label = getResources().getString(R.string.pip_menu_title); + info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, label)); + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (action == ACTION_CLICK && mMenuState == MENU_STATE_CLOSE) { + mController.onPipShowMenu(); + } + return super.performAccessibilityAction(host, action, args); + } + }); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_ESCAPE) { + hideMenu(); + return true; + } + return super.onKeyUp(keyCode, event); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (!mAllowTouches) { + return false; + } + + if (mAllowMenuTimeout) { + repostDelayedHide(POST_INTERACTION_DISMISS_DELAY); + } + + return super.dispatchTouchEvent(ev); + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent event) { + if (mAllowMenuTimeout) { + repostDelayedHide(POST_INTERACTION_DISMISS_DELAY); + } + + return super.dispatchGenericMotionEvent(event); + } + + void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout, + boolean resizeMenuOnShow, boolean withDelay, boolean showResizeHandle) { + mAllowMenuTimeout = allowMenuTimeout; + if (mMenuState != menuState) { + // Disallow touches if the menu needs to resize while showing, and we are transitioning + // to/from a full menu state. + boolean disallowTouchesUntilAnimationEnd = resizeMenuOnShow + && (mMenuState == MENU_STATE_FULL || menuState == MENU_STATE_FULL); + mAllowTouches = !disallowTouchesUntilAnimationEnd; + cancelDelayedHide(); + updateActionViews(stackBounds); + if (mMenuContainerAnimator != null) { + mMenuContainerAnimator.cancel(); + } + mMenuContainerAnimator = new AnimatorSet(); + ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA, + mMenuContainer.getAlpha(), 1f); + menuAnim.addUpdateListener(mMenuBgUpdateListener); + ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA, + mSettingsButton.getAlpha(), 1f); + ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA, + mDismissButton.getAlpha(), 1f); + ObjectAnimator resizeAnim = ObjectAnimator.ofFloat(mResizeHandle, View.ALPHA, + mResizeHandle.getAlpha(), + ENABLE_RESIZE_HANDLE && menuState == MENU_STATE_CLOSE && showResizeHandle + ? 1f : 0f); + if (menuState == MENU_STATE_FULL) { + mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim, + resizeAnim); + } else { + mMenuContainerAnimator.playTogether(dismissAnim, resizeAnim); + } + mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_IN); + mMenuContainerAnimator.setDuration(menuState == MENU_STATE_CLOSE + ? MENU_FADE_DURATION + : MENU_SLOW_FADE_DURATION); + if (allowMenuTimeout) { + mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + repostDelayedHide(INITIAL_DISMISS_DELAY); + } + }); + } + if (withDelay) { + // starts the menu container animation after window expansion is completed + notifyMenuStateChange(menuState, resizeMenuOnShow, () -> { + if (mMenuContainerAnimator == null) { + return; + } + mMenuContainerAnimator.setStartDelay(MENU_SHOW_ON_EXPAND_START_DELAY); + mMenuContainerAnimator.start(); + }); + } else { + notifyMenuStateChange(menuState, resizeMenuOnShow, null); + mMenuContainerAnimator.start(); + } + } else { + // If we are already visible, then just start the delayed dismiss and unregister any + // existing input consumers from the previous drag + if (allowMenuTimeout) { + repostDelayedHide(POST_INTERACTION_DISMISS_DELAY); + } + } + } + + /** + * Different from {@link #hideMenu()}, this function does not try to finish this menu activity + * and instead, it fades out the controls by setting the alpha to 0 directly without menu + * visibility callbacks invoked. + */ + void fadeOutMenu() { + mMenuContainer.setAlpha(0f); + mSettingsButton.setAlpha(0f); + mDismissButton.setAlpha(0f); + mResizeHandle.setAlpha(0f); + } + + void pokeMenu() { + cancelDelayedHide(); + } + + void onPipAnimationEnded() { + mAllowTouches = true; + } + + void updateMenuLayout(Rect bounds) { + mPipMenuIconsAlgorithm.onBoundsChanged(bounds); + } + + void hideMenu() { + hideMenu(null); + } + + void hideMenu(Runnable animationEndCallback) { + hideMenu(animationEndCallback, true /* notifyMenuVisibility */, true /* animate */); + } + + private void hideMenu(final Runnable animationFinishedRunnable, boolean notifyMenuVisibility, + boolean animate) { + if (mMenuState != MENU_STATE_NONE) { + cancelDelayedHide(); + if (notifyMenuVisibility) { + notifyMenuStateChange(MENU_STATE_NONE, mResize, null); + } + mMenuContainerAnimator = new AnimatorSet(); + ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA, + mMenuContainer.getAlpha(), 0f); + menuAnim.addUpdateListener(mMenuBgUpdateListener); + ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA, + mSettingsButton.getAlpha(), 0f); + ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA, + mDismissButton.getAlpha(), 0f); + ObjectAnimator resizeAnim = ObjectAnimator.ofFloat(mResizeHandle, View.ALPHA, + mResizeHandle.getAlpha(), 0f); + mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim, resizeAnim); + mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_OUT); + mMenuContainerAnimator.setDuration(animate ? MENU_FADE_DURATION : 0); + mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (animationFinishedRunnable != null) { + animationFinishedRunnable.run(); + } + } + }); + mMenuContainerAnimator.start(); + } + } + + void setActions(Rect stackBounds, List<RemoteAction> actions) { + mActions.clear(); + mActions.addAll(actions); + updateActionViews(stackBounds); + } + + private void updateActionViews(Rect stackBounds) { + ViewGroup expandContainer = findViewById(R.id.expand_container); + ViewGroup actionsContainer = findViewById(R.id.actions_container); + actionsContainer.setOnTouchListener((v, ev) -> { + // Do nothing, prevent click through to parent + return true; + }); + + if (mActions.isEmpty() || mMenuState == MENU_STATE_CLOSE) { + actionsContainer.setVisibility(View.INVISIBLE); + } else { + actionsContainer.setVisibility(View.VISIBLE); + if (mActionsGroup != null) { + // Ensure we have as many buttons as actions + final LayoutInflater inflater = LayoutInflater.from(mContext); + while (mActionsGroup.getChildCount() < mActions.size()) { + final ImageButton actionView = (ImageButton) inflater.inflate( + R.layout.pip_menu_action, mActionsGroup, false); + mActionsGroup.addView(actionView); + } + + // Update the visibility of all views + for (int i = 0; i < mActionsGroup.getChildCount(); i++) { + mActionsGroup.getChildAt(i).setVisibility(i < mActions.size() + ? View.VISIBLE + : View.GONE); + } + + // Recreate the layout + final boolean isLandscapePip = stackBounds != null + && (stackBounds.width() > stackBounds.height()); + for (int i = 0; i < mActions.size(); i++) { + final RemoteAction action = mActions.get(i); + final ImageButton actionView = (ImageButton) mActionsGroup.getChildAt(i); + + // TODO: Check if the action drawable has changed before we reload it + action.getIcon().loadDrawableAsync(mContext, d -> { + if (d != null) { + d.setTint(Color.WHITE); + actionView.setImageDrawable(d); + } + }, mHandler); + actionView.setContentDescription(action.getContentDescription()); + if (action.isEnabled()) { + actionView.setOnClickListener(v -> { + mHandler.post(() -> { + try { + action.getActionIntent().send(); + } catch (CanceledException e) { + Log.w(TAG, "Failed to send action", e); + } + }); + }); + } + actionView.setEnabled(action.isEnabled()); + actionView.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA); + + // Update the margin between actions + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) + actionView.getLayoutParams(); + lp.leftMargin = (isLandscapePip && i > 0) ? mBetweenActionPaddingLand : 0; + } + } + + // Update the expand container margin to adjust the center of the expand button to + // account for the existence of the action container + FrameLayout.LayoutParams expandedLp = + (FrameLayout.LayoutParams) expandContainer.getLayoutParams(); + expandedLp.topMargin = getResources().getDimensionPixelSize( + R.dimen.pip_action_padding); + expandedLp.bottomMargin = getResources().getDimensionPixelSize( + R.dimen.pip_expand_container_edge_margin); + expandContainer.requestLayout(); + } + } + + private void notifyMenuStateChange(int menuState, boolean resize, Runnable callback) { + mMenuState = menuState; + mController.onMenuStateChanged(menuState, resize, callback); + } + + private void expandPip() { + // Do not notify menu visibility when hiding the menu, the controller will do this when it + // handles the message + hideMenu(mController::onPipExpand, false /* notifyMenuVisibility */, true /* animate */); + } + + private void dismissPip() { + // Since tapping on the close-button invokes a double-tap wait callback in PipTouchHandler, + // we want to disable animating the fadeout animation of the buttons in order to call on + // PipTouchHandler#onPipDismiss fast enough. + final boolean animate = mMenuState != MENU_STATE_CLOSE; + // Do not notify menu visibility when hiding the menu, the controller will do this when it + // handles the message + hideMenu(mController::onPipDismiss, false /* notifyMenuVisibility */, animate); + } + + private void showSettings() { + final Pair<ComponentName, Integer> topPipActivityInfo = + PipUtils.getTopPipActivity(mContext, ActivityManager.getService()); + if (topPipActivityInfo.first != null) { + final Intent settingsIntent = new Intent(ACTION_PICTURE_IN_PICTURE_SETTINGS, + Uri.fromParts("package", topPipActivityInfo.first.getPackageName(), null)); + settingsIntent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK); + mContext.startActivityAsUser(settingsIntent, UserHandle.CURRENT); + } + } + + private void cancelDelayedHide() { + mHandler.removeCallbacks(mHideMenuRunnable); + } + + private void repostDelayedHide(int delay) { + int recommendedTimeout = mAccessibilityManager.getRecommendedTimeoutMillis(delay, + FLAG_CONTENT_ICONS | FLAG_CONTENT_CONTROLS); + mHandler.removeCallbacks(mHideMenuRunnable); + mHandler.postDelayed(mHideMenuRunnable, recommendedTimeout); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java new file mode 100644 index 000000000000..b5fa03082401 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java @@ -0,0 +1,662 @@ +/* + * 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.pip.phone; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Debug; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.Choreographer; + +import androidx.annotation.VisibleForTesting; +import androidx.dynamicanimation.animation.AnimationHandler; +import androidx.dynamicanimation.animation.AnimationHandler.FrameCallbackScheduler; +import androidx.dynamicanimation.animation.SpringForce; + +import com.android.wm.shell.animation.FloatProperties; +import com.android.wm.shell.animation.PhysicsAnimator; +import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; +import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipSnapAlgorithm; +import com.android.wm.shell.pip.PipTaskOrganizer; + +import java.io.PrintWriter; +import java.util.function.Consumer; + +import kotlin.Unit; +import kotlin.jvm.functions.Function0; + +/** + * A helper to animate and manipulate the PiP. + */ +public class PipMotionHelper implements PipAppOpsListener.Callback, + FloatingContentCoordinator.FloatingContent { + + private static final String TAG = "PipMotionHelper"; + private static final boolean DEBUG = false; + + private static final int SHRINK_STACK_FROM_MENU_DURATION = 250; + private static final int EXPAND_STACK_TO_MENU_DURATION = 250; + private static final int LEAVE_PIP_DURATION = 300; + private static final int SHIFT_DURATION = 300; + private static final float STASH_RATIO = 0.25f; + + /** Friction to use for PIP when it moves via physics fling animations. */ + private static final float DEFAULT_FRICTION = 2f; + + private final Context mContext; + private final PipTaskOrganizer mPipTaskOrganizer; + private final @NonNull PipBoundsState mPipBoundsState; + + private PipMenuActivityController mMenuController; + private PipSnapAlgorithm mSnapAlgorithm; + + private final Handler mMainHandler = new Handler(Looper.getMainLooper()); + + /** PIP's current bounds on the screen. */ + private final Rect mBounds = new Rect(); + + /** The bounds within which PIP's top-left coordinate is allowed to move. */ + private final Rect mMovementBounds = new Rect(); + + /** The region that all of PIP must stay within. */ + private final Rect mFloatingAllowedArea = new Rect(); + + /** + * Temporary bounds used when PIP is being dragged or animated. These bounds are applied to PIP + * using {@link PipTaskOrganizer#scheduleUserResizePip}, so that we can animate shrinking into + * and expanding out of the magnetic dismiss target. + * + * Once PIP is done being dragged or animated, we set {@link #mBounds} equal to these temporary + * bounds, and call {@link PipTaskOrganizer#scheduleFinishResizePip} to 'officially' move PIP to + * its new bounds. + */ + private final Rect mTemporaryBounds = new Rect(); + + /** The destination bounds to which PIP is animating. */ + private final Rect mAnimatingToBounds = new Rect(); + + /** Coordinator instance for resolving conflicts with other floating content. */ + private FloatingContentCoordinator mFloatingContentCoordinator; + + private ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal = + ThreadLocal.withInitial(() -> { + FrameCallbackScheduler scheduler = runnable -> + Choreographer.getSfInstance().postFrameCallback(t -> runnable.run()); + AnimationHandler handler = new AnimationHandler(scheduler); + return handler; + }); + + /** + * PhysicsAnimator instance for animating {@link #mTemporaryBounds} using physics animations. + */ + private PhysicsAnimator<Rect> mTemporaryBoundsPhysicsAnimator = PhysicsAnimator.getInstance( + mTemporaryBounds); + + private MagnetizedObject<Rect> mMagnetizedPip; + + /** + * Update listener that resizes the PIP to {@link #mTemporaryBounds}. + */ + private final PhysicsAnimator.UpdateListener<Rect> mResizePipUpdateListener; + + /** FlingConfig instances provided to PhysicsAnimator for fling gestures. */ + private PhysicsAnimator.FlingConfig mFlingConfigX; + private PhysicsAnimator.FlingConfig mFlingConfigY; + /** FlingConfig instances proviced to PhysicsAnimator for stashing. */ + private PhysicsAnimator.FlingConfig mStashConfigX; + + /** SpringConfig to use for fling-then-spring animations. */ + private final PhysicsAnimator.SpringConfig mSpringConfig = + new PhysicsAnimator.SpringConfig( + SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY); + + /** SpringConfig to use for springing PIP away from conflicting floating content. */ + private final PhysicsAnimator.SpringConfig mConflictResolutionSpringConfig = + new PhysicsAnimator.SpringConfig( + SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY); + + private final Consumer<Rect> mUpdateBoundsCallback = (Rect newBounds) -> { + mMainHandler.post(() -> { + mMenuController.updateMenuLayout(newBounds); + mBounds.set(newBounds); + }); + }; + + /** + * Whether we're springing to the touch event location (vs. moving it to that position + * instantly). We spring-to-touch after PIP is dragged out of the magnetic target, since it was + * 'stuck' in the target and needs to catch up to the touch location. + */ + private boolean mSpringingToTouch = false; + + /** + * Whether PIP was released in the dismiss target, and will be animated out and dismissed + * shortly. + */ + private boolean mDismissalPending = false; + + /** + * Gets set in {@link #animateToExpandedState(Rect, Rect, Rect, Runnable)}, this callback is + * used to show menu activity when the expand animation is completed. + */ + private Runnable mPostPipTransitionCallback; + + private final PipTaskOrganizer.PipTransitionCallback mPipTransitionCallback = + new PipTaskOrganizer.PipTransitionCallback() { + @Override + public void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds) {} + + @Override + public void onPipTransitionFinished(ComponentName activity, int direction) { + if (mPostPipTransitionCallback != null) { + mPostPipTransitionCallback.run(); + mPostPipTransitionCallback = null; + } + } + + @Override + public void onPipTransitionCanceled(ComponentName activity, int direction) {} + }; + + public PipMotionHelper(Context context, @NonNull PipBoundsState pipBoundsState, + PipTaskOrganizer pipTaskOrganizer, PipMenuActivityController menuController, + PipSnapAlgorithm snapAlgorithm, FloatingContentCoordinator floatingContentCoordinator) { + mContext = context; + mPipTaskOrganizer = pipTaskOrganizer; + mPipBoundsState = pipBoundsState; + mMenuController = menuController; + mSnapAlgorithm = snapAlgorithm; + mFloatingContentCoordinator = floatingContentCoordinator; + mPipTaskOrganizer.registerPipTransitionCallback(mPipTransitionCallback); + mTemporaryBoundsPhysicsAnimator.setCustomAnimationHandler( + mSfAnimationHandlerThreadLocal.get()); + + mResizePipUpdateListener = (target, values) -> { + if (!mTemporaryBounds.isEmpty()) { + mPipTaskOrganizer.scheduleUserResizePip( + mBounds, mTemporaryBounds, null); + } + }; + } + + @NonNull + @Override + public Rect getFloatingBoundsOnScreen() { + return !mAnimatingToBounds.isEmpty() ? mAnimatingToBounds : mBounds; + } + + @NonNull + @Override + public Rect getAllowedFloatingBoundsRegion() { + return mFloatingAllowedArea; + } + + @Override + public void moveToBounds(@NonNull Rect bounds) { + animateToBounds(bounds, mConflictResolutionSpringConfig); + } + + /** + * Synchronizes the current bounds with the pinned stack, cancelling any ongoing animations. + */ + void synchronizePinnedStackBounds() { + cancelAnimations(); + mBounds.set(mPipBoundsState.getBounds()); + mTemporaryBounds.setEmpty(); + + if (mPipTaskOrganizer.isInPip()) { + mFloatingContentCoordinator.onContentMoved(this); + } + } + + boolean isAnimating() { + return mTemporaryBoundsPhysicsAnimator.isRunning(); + } + + /** + * Tries to move the pinned stack to the given {@param bounds}. + */ + void movePip(Rect toBounds) { + movePip(toBounds, false /* isDragging */); + } + + /** + * Tries to move the pinned stack to the given {@param bounds}. + * + * @param isDragging Whether this movement is the result of a drag touch gesture. If so, we + * won't notify the floating content coordinator of this move, since that will + * happen when the gesture ends. + */ + void movePip(Rect toBounds, boolean isDragging) { + if (!isDragging) { + mFloatingContentCoordinator.onContentMoved(this); + } + + if (!mSpringingToTouch) { + // If we are moving PIP directly to the touch event locations, cancel any animations and + // move PIP to the given bounds. + cancelAnimations(); + + if (!isDragging) { + resizePipUnchecked(toBounds); + mBounds.set(toBounds); + } else { + mTemporaryBounds.set(toBounds); + mPipTaskOrganizer.scheduleUserResizePip(mBounds, mTemporaryBounds, + (Rect newBounds) -> { + mMainHandler.post(() -> { + mMenuController.updateMenuLayout(newBounds); + }); + }); + } + } else { + // If PIP is 'catching up' after being stuck in the dismiss target, update the animation + // to spring towards the new touch location. + mTemporaryBoundsPhysicsAnimator + .spring(FloatProperties.RECT_WIDTH, mBounds.width(), mSpringConfig) + .spring(FloatProperties.RECT_HEIGHT, mBounds.height(), mSpringConfig) + .spring(FloatProperties.RECT_X, toBounds.left, mSpringConfig) + .spring(FloatProperties.RECT_Y, toBounds.top, mSpringConfig); + + startBoundsAnimator(toBounds.left /* toX */, toBounds.top /* toY */, + false /* dismiss */); + } + } + + /** Animates the PIP into the dismiss target, scaling it down. */ + void animateIntoDismissTarget( + MagnetizedObject.MagneticTarget target, + float velX, float velY, + boolean flung, Function0<Unit> after) { + final PointF targetCenter = target.getCenterOnScreen(); + + final float desiredWidth = mBounds.width() / 2; + final float desiredHeight = mBounds.height() / 2; + + final float destinationX = targetCenter.x - (desiredWidth / 2f); + final float destinationY = targetCenter.y - (desiredHeight / 2f); + + // If we're already in the dismiss target area, then there won't be a move to set the + // temporary bounds, so just initialize it to the current bounds + if (mTemporaryBounds.isEmpty()) { + mTemporaryBounds.set(mBounds); + } + mTemporaryBoundsPhysicsAnimator + .spring(FloatProperties.RECT_X, destinationX, velX, mSpringConfig) + .spring(FloatProperties.RECT_Y, destinationY, velY, mSpringConfig) + .spring(FloatProperties.RECT_WIDTH, desiredWidth, mSpringConfig) + .spring(FloatProperties.RECT_HEIGHT, desiredHeight, mSpringConfig) + .withEndActions(after); + + startBoundsAnimator(destinationX, destinationY, false); + } + + /** Set whether we're springing-to-touch to catch up after being stuck in the dismiss target. */ + void setSpringingToTouch(boolean springingToTouch) { + mSpringingToTouch = springingToTouch; + } + + /** + * Resizes the pinned stack back to unknown windowing mode, which could be freeform or + * * fullscreen depending on the display area's windowing mode. + */ + void expandLeavePip() { + expandLeavePip(false /* skipAnimation */); + } + + /** + * Resizes the pinned stack back to unknown windowing mode, which could be freeform or + * fullscreen depending on the display area's windowing mode. + */ + void expandLeavePip(boolean skipAnimation) { + if (DEBUG) { + Log.d(TAG, "exitPip: skipAnimation=" + skipAnimation + + " callers=\n" + Debug.getCallers(5, " ")); + } + cancelAnimations(); + mMenuController.hideMenuWithoutResize(); + mPipTaskOrganizer.getUpdateHandler().post(() -> { + mPipTaskOrganizer.exitPip(skipAnimation + ? 0 + : LEAVE_PIP_DURATION); + }); + } + + /** + * Dismisses the pinned stack. + */ + @Override + public void dismissPip() { + if (DEBUG) { + Log.d(TAG, "removePip: callers=\n" + Debug.getCallers(5, " ")); + } + cancelAnimations(); + mMenuController.hideMenuWithoutResize(); + mPipTaskOrganizer.removePip(); + } + + /** Sets the movement bounds to use to constrain PIP position animations. */ + void setCurrentMovementBounds(Rect movementBounds) { + mMovementBounds.set(movementBounds); + rebuildFlingConfigs(); + + // The movement bounds represent the area within which we can move PIP's top-left position. + // The allowed area for all of PIP is those bounds plus PIP's width and height. + mFloatingAllowedArea.set(mMovementBounds); + mFloatingAllowedArea.right += mBounds.width(); + mFloatingAllowedArea.bottom += mBounds.height(); + } + + /** + * @return the PiP bounds. + */ + Rect getBounds() { + return mBounds; + } + + /** + * Returns the PIP bounds if we're not animating, or the current, temporary animating bounds + * otherwise. + */ + Rect getPossiblyAnimatingBounds() { + return mTemporaryBounds.isEmpty() ? mBounds : mTemporaryBounds; + } + + /** + * Flings the PiP to the closest snap target. + */ + void flingToSnapTarget( + float velocityX, float velocityY, @Nullable Runnable endAction) { + movetoTarget(velocityX, velocityY, endAction, false /* isStash */); + } + + /** + * Stash PiP to the closest edge. + */ + void stashToEdge( + float velocityX, float velocityY, @Nullable Runnable endAction) { + movetoTarget(velocityX, velocityY, endAction, true /* isStash */); + } + + private void movetoTarget( + float velocityX, float velocityY, @Nullable Runnable endAction, boolean isStash) { + // If we're flinging to a snap target now, we're not springing to catch up to the touch + // location now. + mSpringingToTouch = false; + + mTemporaryBoundsPhysicsAnimator + .spring(FloatProperties.RECT_WIDTH, mBounds.width(), mSpringConfig) + .spring(FloatProperties.RECT_HEIGHT, mBounds.height(), mSpringConfig) + .flingThenSpring( + FloatProperties.RECT_X, velocityX, isStash ? mStashConfigX : mFlingConfigX, + mSpringConfig, true /* flingMustReachMinOrMax */) + .flingThenSpring( + FloatProperties.RECT_Y, velocityY, mFlingConfigY, mSpringConfig) + .withEndActions(endAction); + + final float offset = ((float) mBounds.width()) * (1.0f - STASH_RATIO); + final float leftEdge = isStash ? mMovementBounds.left - offset : mMovementBounds.left; + final float rightEdge = isStash ? mMovementBounds.right + offset : mMovementBounds.right; + + final float xEndValue = velocityX < 0 ? leftEdge : rightEdge; + final float estimatedFlingYEndValue = + PhysicsAnimator.estimateFlingEndValue( + mTemporaryBounds.top, velocityY, mFlingConfigY); + + startBoundsAnimator(xEndValue /* toX */, estimatedFlingYEndValue /* toY */, + false /* dismiss */); + } + + /** + * Animates PIP to the provided bounds, using physics animations and the given spring + * configuration + */ + void animateToBounds(Rect bounds, PhysicsAnimator.SpringConfig springConfig) { + if (!mTemporaryBoundsPhysicsAnimator.isRunning()) { + // Animate from the current bounds if we're not already animating. + mTemporaryBounds.set(mBounds); + } + + mTemporaryBoundsPhysicsAnimator + .spring(FloatProperties.RECT_X, bounds.left, springConfig) + .spring(FloatProperties.RECT_Y, bounds.top, springConfig); + startBoundsAnimator(bounds.left /* toX */, bounds.top /* toY */, + false /* dismiss */); + } + + /** + * Animates the dismissal of the PiP off the edge of the screen. + */ + void animateDismiss() { + // Animate off the bottom of the screen, then dismiss PIP. + mTemporaryBoundsPhysicsAnimator + .spring(FloatProperties.RECT_Y, + mMovementBounds.bottom + mBounds.height() * 2, + 0, + mSpringConfig) + .withEndActions(this::dismissPip); + + startBoundsAnimator( + mBounds.left /* toX */, mBounds.bottom + mBounds.height() /* toY */, + true /* dismiss */); + + mDismissalPending = false; + } + + /** + * Animates the PiP to the expanded state to show the menu. + */ + float animateToExpandedState(Rect expandedBounds, Rect movementBounds, + Rect expandedMovementBounds, Runnable callback) { + float savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(mBounds), movementBounds); + mSnapAlgorithm.applySnapFraction(expandedBounds, expandedMovementBounds, savedSnapFraction); + mPostPipTransitionCallback = callback; + resizeAndAnimatePipUnchecked(expandedBounds, EXPAND_STACK_TO_MENU_DURATION); + return savedSnapFraction; + } + + /** + * Animates the PiP from the expanded state to the normal state after the menu is hidden. + */ + void animateToUnexpandedState(Rect normalBounds, float savedSnapFraction, + Rect normalMovementBounds, Rect currentMovementBounds, boolean immediate) { + if (savedSnapFraction < 0f) { + // If there are no saved snap fractions, then just use the current bounds + savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(mBounds), + currentMovementBounds); + } + mSnapAlgorithm.applySnapFraction(normalBounds, normalMovementBounds, savedSnapFraction); + + if (immediate) { + movePip(normalBounds); + } else { + resizeAndAnimatePipUnchecked(normalBounds, SHRINK_STACK_FROM_MENU_DURATION); + } + } + + /** + * Animates the PiP to offset it from the IME or shelf. + */ + @VisibleForTesting + public void animateToOffset(Rect originalBounds, int offset) { + if (DEBUG) { + Log.d(TAG, "animateToOffset: originalBounds=" + originalBounds + " offset=" + offset + + " callers=\n" + Debug.getCallers(5, " ")); + } + cancelAnimations(); + mPipTaskOrganizer.scheduleOffsetPip(originalBounds, offset, SHIFT_DURATION, + mUpdateBoundsCallback); + } + + /** + * Cancels all existing animations. + */ + private void cancelAnimations() { + mTemporaryBoundsPhysicsAnimator.cancel(); + mAnimatingToBounds.setEmpty(); + mSpringingToTouch = false; + } + + /** Set new fling configs whose min/max values respect the given movement bounds. */ + private void rebuildFlingConfigs() { + mFlingConfigX = new PhysicsAnimator.FlingConfig( + DEFAULT_FRICTION, mMovementBounds.left, mMovementBounds.right); + mFlingConfigY = new PhysicsAnimator.FlingConfig( + DEFAULT_FRICTION, mMovementBounds.top, mMovementBounds.bottom); + final float offset = ((float) mBounds.width()) * (1.0f - STASH_RATIO); + mStashConfigX = new PhysicsAnimator.FlingConfig( + DEFAULT_FRICTION, mMovementBounds.left - offset, mMovementBounds.right + offset); + } + + /** + * Starts the physics animator which will update the animated PIP bounds using physics + * animations, as well as the TimeAnimator which will apply those bounds to PIP. + * + * This will also add end actions to the bounds animator that cancel the TimeAnimator and update + * the 'real' bounds to equal the final animated bounds. + */ + private void startBoundsAnimator(float toX, float toY, boolean dismiss) { + if (!mSpringingToTouch) { + cancelAnimations(); + } + + // Set animatingToBounds directly to avoid allocating a new Rect, but then call + // setAnimatingToBounds to run the normal logic for changing animatingToBounds. + mAnimatingToBounds.set( + (int) toX, + (int) toY, + (int) toX + mBounds.width(), + (int) toY + mBounds.height()); + setAnimatingToBounds(mAnimatingToBounds); + + if (!mTemporaryBoundsPhysicsAnimator.isRunning()) { + mTemporaryBoundsPhysicsAnimator + .addUpdateListener(mResizePipUpdateListener) + .withEndActions(this::onBoundsAnimationEnd); + } + + mTemporaryBoundsPhysicsAnimator.start(); + } + + /** + * Notify that PIP was released in the dismiss target and will be animated out and dismissed + * shortly. + */ + void notifyDismissalPending() { + mDismissalPending = true; + } + + private void onBoundsAnimationEnd() { + if (!mDismissalPending + && !mSpringingToTouch + && !mMagnetizedPip.getObjectStuckToTarget()) { + mBounds.set(mTemporaryBounds); + if (!mDismissalPending) { + // do not schedule resize if PiP is dismissing, which may cause app re-open to + // mBounds instead of it's normal bounds. + mPipTaskOrganizer.scheduleFinishResizePip(mBounds); + } + mTemporaryBounds.setEmpty(); + } + + mAnimatingToBounds.setEmpty(); + mSpringingToTouch = false; + mDismissalPending = false; + } + + /** + * Notifies the floating coordinator that we're moving, and sets {@link #mAnimatingToBounds} so + * we return these bounds from + * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}. + */ + private void setAnimatingToBounds(Rect bounds) { + mAnimatingToBounds.set(bounds); + mFloatingContentCoordinator.onContentMoved(this); + } + + /** + * Directly resizes the PiP to the given {@param bounds}. + */ + private void resizePipUnchecked(Rect toBounds) { + if (DEBUG) { + Log.d(TAG, "resizePipUnchecked: toBounds=" + toBounds + + " callers=\n" + Debug.getCallers(5, " ")); + } + if (!toBounds.equals(mBounds)) { + mPipTaskOrganizer.scheduleResizePip(toBounds, mUpdateBoundsCallback); + } + } + + /** + * Directly resizes the PiP to the given {@param bounds}. + */ + private void resizeAndAnimatePipUnchecked(Rect toBounds, int duration) { + if (DEBUG) { + Log.d(TAG, "resizeAndAnimatePipUnchecked: toBounds=" + toBounds + + " duration=" + duration + " callers=\n" + Debug.getCallers(5, " ")); + } + + // Intentionally resize here even if the current bounds match the destination bounds. + // This is so all the proper callbacks are performed. + mPipTaskOrganizer.scheduleAnimateResizePip(toBounds, duration, mUpdateBoundsCallback); + setAnimatingToBounds(toBounds); + } + + /** + * Returns a MagnetizedObject wrapper for PIP's animated bounds. This is provided to the + * magnetic dismiss target so it can calculate PIP's size and position. + */ + MagnetizedObject<Rect> getMagnetizedPip() { + if (mMagnetizedPip == null) { + mMagnetizedPip = new MagnetizedObject<Rect>( + mContext, mTemporaryBounds, FloatProperties.RECT_X, FloatProperties.RECT_Y) { + @Override + public float getWidth(@NonNull Rect animatedPipBounds) { + return animatedPipBounds.width(); + } + + @Override + public float getHeight(@NonNull Rect animatedPipBounds) { + return animatedPipBounds.height(); + } + + @Override + public void getLocationOnScreen( + @NonNull Rect animatedPipBounds, @NonNull int[] loc) { + loc[0] = animatedPipBounds.left; + loc[1] = animatedPipBounds.top; + } + }; + } + + return mMagnetizedPip; + } + + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mBounds=" + mBounds); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java new file mode 100644 index 000000000000..ef3875597aa2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java @@ -0,0 +1,523 @@ +/* + * 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.pip.phone; + +import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_PINCH_RESIZE; +import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_BOTTOM; +import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_LEFT; +import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_NONE; +import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_RIGHT; +import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_TOP; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.Region; +import android.hardware.input.InputManager; +import android.os.Handler; +import android.os.Looper; +import android.provider.DeviceConfig; +import android.view.BatchedInputEventReceiver; +import android.view.Choreographer; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.InputMonitor; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.ViewConfiguration; + +import androidx.annotation.VisibleForTesting; + +import com.android.internal.policy.TaskResizingAlgorithm; +import com.android.wm.shell.R; +import com.android.wm.shell.pip.PipBoundsHandler; +import com.android.wm.shell.pip.PipTaskOrganizer; +import com.android.wm.shell.pip.PipUiEventLogger; + +import java.io.PrintWriter; +import java.util.concurrent.Executor; +import java.util.function.Function; + +/** + * Helper on top of PipTouchHandler that handles inputs OUTSIDE of the PIP window, which is used to + * trigger dynamic resize. + */ +public class PipResizeGestureHandler { + + private static final String TAG = "PipResizeGestureHandler"; + private static final float PINCH_THRESHOLD = 0.05f; + private static final float STARTING_SCALE_FACTOR = 1.0f; + + private final Context mContext; + private final PipBoundsHandler mPipBoundsHandler; + private final PipMotionHelper mMotionHelper; + private final int mDisplayId; + private final Executor mMainExecutor; + private final ScaleGestureDetector mScaleGestureDetector; + private final Region mTmpRegion = new Region(); + + private final PointF mDownPoint = new PointF(); + private final Point mMaxSize = new Point(); + private final Point mMinSize = new Point(); + private final Rect mLastResizeBounds = new Rect(); + private final Rect mUserResizeBounds = new Rect(); + private final Rect mLastDownBounds = new Rect(); + private final Rect mDragCornerSize = new Rect(); + private final Rect mTmpTopLeftCorner = new Rect(); + private final Rect mTmpTopRightCorner = new Rect(); + private final Rect mTmpBottomLeftCorner = new Rect(); + private final Rect mTmpBottomRightCorner = new Rect(); + private final Rect mDisplayBounds = new Rect(); + private final Function<Rect, Rect> mMovementBoundsSupplier; + private final Runnable mUpdateMovementBoundsRunnable; + + private int mDelta; + private float mTouchSlop; + private boolean mAllowGesture; + private boolean mIsAttached; + private boolean mIsEnabled; + private boolean mEnablePinchResize; + private boolean mIsSysUiStateValid; + private boolean mThresholdCrossed; + private boolean mUsingPinchToZoom = false; + private float mScaleFactor = STARTING_SCALE_FACTOR; + + private InputMonitor mInputMonitor; + private InputEventReceiver mInputEventReceiver; + private PipTaskOrganizer mPipTaskOrganizer; + private PipMenuActivityController mPipMenuActivityController; + private PipUiEventLogger mPipUiEventLogger; + + private int mCtrlType; + + public PipResizeGestureHandler(Context context, PipBoundsHandler pipBoundsHandler, + PipMotionHelper motionHelper, PipTaskOrganizer pipTaskOrganizer, + Function<Rect, Rect> movementBoundsSupplier, Runnable updateMovementBoundsRunnable, + PipUiEventLogger pipUiEventLogger, PipMenuActivityController menuActivityController) { + mContext = context; + mDisplayId = context.getDisplayId(); + mMainExecutor = context.getMainExecutor(); + mPipBoundsHandler = pipBoundsHandler; + mMotionHelper = motionHelper; + mPipTaskOrganizer = pipTaskOrganizer; + mMovementBoundsSupplier = movementBoundsSupplier; + mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable; + mPipMenuActivityController = menuActivityController; + mPipUiEventLogger = pipUiEventLogger; + + context.getDisplay().getRealSize(mMaxSize); + reloadResources(); + + mScaleGestureDetector = new ScaleGestureDetector(context, + new ScaleGestureDetector.OnScaleGestureListener() { + @Override + public boolean onScale(ScaleGestureDetector detector) { + mScaleFactor *= detector.getScaleFactor(); + + if (!mThresholdCrossed + && (mScaleFactor > (STARTING_SCALE_FACTOR + PINCH_THRESHOLD) + || mScaleFactor < (STARTING_SCALE_FACTOR - PINCH_THRESHOLD))) { + mThresholdCrossed = true; + mInputMonitor.pilferPointers(); + } + if (mThresholdCrossed) { + int height = Math.min(mMaxSize.y, Math.max(mMinSize.y, + (int) (mScaleFactor * mLastDownBounds.height()))); + int width = Math.min(mMaxSize.x, Math.max(mMinSize.x, + (int) (mScaleFactor * mLastDownBounds.width()))); + int top, bottom, left, right; + + if ((mCtrlType & CTRL_TOP) != 0) { + top = mLastDownBounds.bottom - height; + bottom = mLastDownBounds.bottom; + } else { + top = mLastDownBounds.top; + bottom = mLastDownBounds.top + height; + } + + if ((mCtrlType & CTRL_LEFT) != 0) { + left = mLastDownBounds.right - width; + right = mLastDownBounds.right; + } else { + left = mLastDownBounds.left; + right = mLastDownBounds.left + width; + } + + mLastResizeBounds.set(left, top, right, bottom); + mPipTaskOrganizer.scheduleUserResizePip(mLastDownBounds, + mLastResizeBounds, + null); + } + return true; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + setCtrlTypeForPinchToZoom(); + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mScaleFactor = STARTING_SCALE_FACTOR; + finishResize(); + } + }); + + mEnablePinchResize = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_SYSTEMUI, + PIP_PINCH_RESIZE, + /* defaultValue = */ false); + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, mMainExecutor, + new DeviceConfig.OnPropertiesChangedListener() { + @Override + public void onPropertiesChanged(DeviceConfig.Properties properties) { + if (properties.getKeyset().contains(PIP_PINCH_RESIZE)) { + mEnablePinchResize = properties.getBoolean( + PIP_PINCH_RESIZE, /* defaultValue = */ false); + } + } + }); + } + + public void onConfigurationChanged() { + reloadResources(); + } + + /** + * Called when SysUI state changed. + * + * @param isSysUiStateValid Is SysUI valid or not. + */ + public void onSystemUiStateChanged(boolean isSysUiStateValid) { + mIsSysUiStateValid = isSysUiStateValid; + } + + private void reloadResources() { + final Resources res = mContext.getResources(); + mDelta = res.getDimensionPixelSize(R.dimen.pip_resize_edge_size); + mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); + } + + private void resetDragCorners() { + mDragCornerSize.set(0, 0, mDelta, mDelta); + mTmpTopLeftCorner.set(mDragCornerSize); + mTmpTopRightCorner.set(mDragCornerSize); + mTmpBottomLeftCorner.set(mDragCornerSize); + mTmpBottomRightCorner.set(mDragCornerSize); + } + + private void disposeInputChannel() { + if (mInputEventReceiver != null) { + mInputEventReceiver.dispose(); + mInputEventReceiver = null; + } + if (mInputMonitor != null) { + mInputMonitor.dispose(); + mInputMonitor = null; + } + } + + void onActivityPinned() { + mIsAttached = true; + updateIsEnabled(); + } + + void onActivityUnpinned() { + mIsAttached = false; + mUserResizeBounds.setEmpty(); + updateIsEnabled(); + } + + private void updateIsEnabled() { + boolean isEnabled = mIsAttached; + if (isEnabled == mIsEnabled) { + return; + } + mIsEnabled = isEnabled; + disposeInputChannel(); + + if (mIsEnabled) { + // Register input event receiver + mInputMonitor = InputManager.getInstance().monitorGestureInput( + "pip-resize", mDisplayId); + mInputEventReceiver = new SysUiInputEventReceiver( + mInputMonitor.getInputChannel(), Looper.getMainLooper()); + } + } + + private void onInputEvent(InputEvent ev) { + if (ev instanceof MotionEvent) { + if (mUsingPinchToZoom) { + mScaleGestureDetector.onTouchEvent((MotionEvent) ev); + } else { + onDragCornerResize((MotionEvent) ev); + } + } + } + + /** + * Check whether the current x,y coordinate is within the region in which drag-resize should + * start. + * This consists of 4 small squares on the 4 corners of the PIP window, a quarter of which + * overlaps with the PIP window while the rest goes outside of the PIP window. + * _ _ _ _ + * |_|_|_________|_|_| + * |_|_| |_|_| + * | PIP | + * | WINDOW | + * _|_ _|_ + * |_|_|_________|_|_| + * |_|_| |_|_| + */ + public boolean isWithinTouchRegion(int x, int y) { + final Rect currentPipBounds = mMotionHelper.getBounds(); + if (currentPipBounds == null) { + return false; + } + resetDragCorners(); + mTmpTopLeftCorner.offset(currentPipBounds.left - mDelta / 2, + currentPipBounds.top - mDelta / 2); + mTmpTopRightCorner.offset(currentPipBounds.right - mDelta / 2, + currentPipBounds.top - mDelta / 2); + mTmpBottomLeftCorner.offset(currentPipBounds.left - mDelta / 2, + currentPipBounds.bottom - mDelta / 2); + mTmpBottomRightCorner.offset(currentPipBounds.right - mDelta / 2, + currentPipBounds.bottom - mDelta / 2); + + mTmpRegion.setEmpty(); + mTmpRegion.op(mTmpTopLeftCorner, Region.Op.UNION); + mTmpRegion.op(mTmpTopRightCorner, Region.Op.UNION); + mTmpRegion.op(mTmpBottomLeftCorner, Region.Op.UNION); + mTmpRegion.op(mTmpBottomRightCorner, Region.Op.UNION); + + return mTmpRegion.contains(x, y); + } + + public boolean isUsingPinchToZoom() { + return mEnablePinchResize; + } + + public boolean willStartResizeGesture(MotionEvent ev) { + if (isInValidSysUiState()) { + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + // Always pass the DOWN event to the ScaleGestureDetector + mScaleGestureDetector.onTouchEvent(ev); + if (isWithinTouchRegion((int) ev.getRawX(), (int) ev.getRawY())) { + return true; + } + break; + + case MotionEvent.ACTION_POINTER_DOWN: + if (mEnablePinchResize && ev.getPointerCount() == 2) { + mUsingPinchToZoom = true; + return true; + } + break; + + default: + break; + } + } + return false; + } + + private void setCtrlTypeForPinchToZoom() { + final Rect currentPipBounds = mMotionHelper.getBounds(); + mLastDownBounds.set(mMotionHelper.getBounds()); + + Rect movementBounds = mMovementBoundsSupplier.apply(currentPipBounds); + mDisplayBounds.set(movementBounds.left, + movementBounds.top, + movementBounds.right + currentPipBounds.width(), + movementBounds.bottom + currentPipBounds.height()); + + if (currentPipBounds.left == mDisplayBounds.left) { + mCtrlType |= CTRL_RIGHT; + } else { + mCtrlType |= CTRL_LEFT; + } + + if (currentPipBounds.top > mDisplayBounds.top + mDisplayBounds.height()) { + mCtrlType |= CTRL_TOP; + } else { + mCtrlType |= CTRL_BOTTOM; + } + } + + private void setCtrlType(int x, int y) { + final Rect currentPipBounds = mMotionHelper.getBounds(); + + Rect movementBounds = mMovementBoundsSupplier.apply(currentPipBounds); + mDisplayBounds.set(movementBounds.left, + movementBounds.top, + movementBounds.right + currentPipBounds.width(), + movementBounds.bottom + currentPipBounds.height()); + + if (mTmpTopLeftCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top + && currentPipBounds.left != mDisplayBounds.left) { + mCtrlType |= CTRL_LEFT; + mCtrlType |= CTRL_TOP; + } + if (mTmpTopRightCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top + && currentPipBounds.right != mDisplayBounds.right) { + mCtrlType |= CTRL_RIGHT; + mCtrlType |= CTRL_TOP; + } + if (mTmpBottomRightCorner.contains(x, y) + && currentPipBounds.bottom != mDisplayBounds.bottom + && currentPipBounds.right != mDisplayBounds.right) { + mCtrlType |= CTRL_RIGHT; + mCtrlType |= CTRL_BOTTOM; + } + if (mTmpBottomLeftCorner.contains(x, y) + && currentPipBounds.bottom != mDisplayBounds.bottom + && currentPipBounds.left != mDisplayBounds.left) { + mCtrlType |= CTRL_LEFT; + mCtrlType |= CTRL_BOTTOM; + } + } + + private boolean isInValidSysUiState() { + return mIsSysUiStateValid; + } + + private void onDragCornerResize(MotionEvent ev) { + int action = ev.getActionMasked(); + float x = ev.getX(); + float y = ev.getY(); + if (action == MotionEvent.ACTION_DOWN) { + final Rect currentPipBounds = mMotionHelper.getBounds(); + mLastResizeBounds.setEmpty(); + mAllowGesture = isInValidSysUiState() && isWithinTouchRegion((int) x, (int) y); + if (mAllowGesture) { + setCtrlType((int) x, (int) y); + mDownPoint.set(x, y); + mLastDownBounds.set(mMotionHelper.getBounds()); + } + if (!currentPipBounds.contains((int) ev.getX(), (int) ev.getY()) + && mPipMenuActivityController.isMenuVisible()) { + mPipMenuActivityController.hideMenu(); + } + + } else if (mAllowGesture) { + switch (action) { + case MotionEvent.ACTION_POINTER_DOWN: + // We do not support multi touch for resizing via drag + mAllowGesture = false; + break; + case MotionEvent.ACTION_MOVE: + // Capture inputs + if (!mThresholdCrossed + && Math.hypot(x - mDownPoint.x, y - mDownPoint.y) > mTouchSlop) { + mThresholdCrossed = true; + // Reset the down to begin resizing from this point + mDownPoint.set(x, y); + mInputMonitor.pilferPointers(); + } + if (mThresholdCrossed) { + if (mPipMenuActivityController.isMenuVisible()) { + mPipMenuActivityController.hideMenuWithoutResize(); + mPipMenuActivityController.hideMenu(); + } + final Rect currentPipBounds = mMotionHelper.getBounds(); + mLastResizeBounds.set(TaskResizingAlgorithm.resizeDrag(x, y, + mDownPoint.x, mDownPoint.y, currentPipBounds, mCtrlType, mMinSize.x, + mMinSize.y, mMaxSize, true, + mLastDownBounds.width() > mLastDownBounds.height())); + mPipBoundsHandler.transformBoundsToAspectRatio(mLastResizeBounds); + mPipTaskOrganizer.scheduleUserResizePip(mLastDownBounds, mLastResizeBounds, + null); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + finishResize(); + break; + } + } + } + + private void finishResize() { + if (!mLastResizeBounds.isEmpty()) { + mUserResizeBounds.set(mLastResizeBounds); + mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds, + (Rect bounds) -> { + new Handler(Looper.getMainLooper()).post(() -> { + mMotionHelper.synchronizePinnedStackBounds(); + mUpdateMovementBoundsRunnable.run(); + resetState(); + }); + }); + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_RESIZE); + } else { + resetState(); + } + } + + private void resetState() { + mCtrlType = CTRL_NONE; + mUsingPinchToZoom = false; + mAllowGesture = false; + mThresholdCrossed = false; + } + + void setUserResizeBounds(Rect bounds) { + mUserResizeBounds.set(bounds); + } + + void invalidateUserResizeBounds() { + mUserResizeBounds.setEmpty(); + } + + Rect getUserResizeBounds() { + return mUserResizeBounds; + } + + @VisibleForTesting public void updateMaxSize(int maxX, int maxY) { + mMaxSize.set(maxX, maxY); + } + + @VisibleForTesting public void updateMinSize(int minX, int minY) { + mMinSize.set(minX, minY); + } + + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mAllowGesture=" + mAllowGesture); + pw.println(innerPrefix + "mIsAttached=" + mIsAttached); + pw.println(innerPrefix + "mIsEnabled=" + mIsEnabled); + pw.println(innerPrefix + "mEnablePinchResize=" + mEnablePinchResize); + pw.println(innerPrefix + "mThresholdCrossed=" + mThresholdCrossed); + } + + class SysUiInputEventReceiver extends BatchedInputEventReceiver { + SysUiInputEventReceiver(InputChannel channel, Looper looper) { + super(channel, looper, Choreographer.getSfInstance()); + } + + public void onInputEvent(InputEvent event) { + PipResizeGestureHandler.this.onInputEvent(event); + finishInputEvent(event, true); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchGesture.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchGesture.java new file mode 100644 index 000000000000..1a3cc8b1c1d2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchGesture.java @@ -0,0 +1,42 @@ +/* + * 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.pip.phone; + +/** + * A generic interface for a touch gesture. + */ +public abstract class PipTouchGesture { + + /** + * Handle the touch down. + */ + public void onDown(PipTouchState touchState) {} + + /** + * Handle the touch move, and return whether the event was consumed. + */ + public boolean onMove(PipTouchState touchState) { + return false; + } + + /** + * Handle the touch up, and return whether the gesture was consumed. + */ + public boolean onUp(PipTouchState touchState) { + return false; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java new file mode 100644 index 000000000000..a2233e5c5874 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java @@ -0,0 +1,893 @@ +/* + * 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.pip.phone; + +import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_STASHING; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; +import static com.android.wm.shell.pip.phone.PipMenuActivityController.MENU_STATE_CLOSE; +import static com.android.wm.shell.pip.phone.PipMenuActivityController.MENU_STATE_FULL; +import static com.android.wm.shell.pip.phone.PipMenuActivityController.MENU_STATE_NONE; + +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.content.ComponentName; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Handler; +import android.os.RemoteException; +import android.provider.DeviceConfig; +import android.util.Log; +import android.util.Size; +import android.view.IPinnedStackController; +import android.view.InputEvent; +import android.view.MotionEvent; +import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityWindowInfo; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.R; +import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.pip.PipAnimationController; +import com.android.wm.shell.pip.PipBoundsHandler; +import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipTaskOrganizer; +import com.android.wm.shell.pip.PipUiEventLogger; + +import java.io.PrintWriter; + +/** + * Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding + * the PIP. + */ +public class PipTouchHandler { + private static final String TAG = "PipTouchHandler"; + + /** Duration of the dismiss scrim fading in/out. */ + private static final int DISMISS_TRANSITION_DURATION_MS = 200; + + /* The multiplier to apply scale the target size by when applying the magnetic field radius */ + private static final float MAGNETIC_FIELD_RADIUS_MULTIPLIER = 1.25f; + + // Allow PIP to resize to a slightly bigger state upon touch + private final boolean mEnableResize; + private final Context mContext; + private final PipBoundsHandler mPipBoundsHandler; + private final @NonNull PipBoundsState mPipBoundsState; + private final PipUiEventLogger mPipUiEventLogger; + private final PipDismissTargetHandler mPipDismissTargetHandler; + + private PipResizeGestureHandler mPipResizeGestureHandler; + private IPinnedStackController mPinnedStackController; + + private final PipMenuActivityController mMenuController; + private final AccessibilityManager mAccessibilityManager; + private boolean mShowPipMenuOnAnimationEnd = false; + + /** + * Whether PIP stash is enabled or not. When enabled, if at the time of fling-release the + * PIP bounds is outside the left/right edge of the screen, it will be shown in "stashed" mode, + * where PIP will only show partially. + */ + private boolean mEnableStash = false; + + // The current movement bounds + private Rect mMovementBounds = new Rect(); + + // The reference inset bounds, used to determine the dismiss fraction + private Rect mInsetBounds = new Rect(); + // The reference bounds used to calculate the normal/expanded target bounds + private Rect mNormalBounds = new Rect(); + @VisibleForTesting public Rect mNormalMovementBounds = new Rect(); + private Rect mExpandedBounds = new Rect(); + @VisibleForTesting public Rect mExpandedMovementBounds = new Rect(); + private int mExpandedShortestEdgeSize; + + // Used to workaround an issue where the WM rotation happens before we are notified, allowing + // us to send stale bounds + private int mDeferResizeToNormalBoundsUntilRotation = -1; + private int mDisplayRotation; + + private Handler mHandler = new Handler(); + + // Behaviour states + private int mMenuState = MENU_STATE_NONE; + private boolean mIsImeShowing; + private int mImeHeight; + private int mImeOffset; + private boolean mIsShelfShowing; + private int mShelfHeight; + private int mMovementBoundsExtraOffsets; + private int mBottomOffsetBufferPx; + private float mSavedSnapFraction = -1f; + private boolean mSendingHoverAccessibilityEvents; + private boolean mMovementWithinDismiss; + private PipAccessibilityInteractionConnection mConnection; + + // Touch state + private final PipTouchState mTouchState; + private final FloatingContentCoordinator mFloatingContentCoordinator; + private PipMotionHelper mMotionHelper; + private PipTouchGesture mGesture; + + // Temp vars + private final Rect mTmpBounds = new Rect(); + + /** + * A listener for the PIP menu activity. + */ + private class PipMenuListener implements PipMenuActivityController.Listener { + @Override + public void onPipMenuStateChanged(int menuState, boolean resize, Runnable callback) { + setMenuState(menuState, resize, callback); + } + + @Override + public void onPipExpand() { + mMotionHelper.expandLeavePip(); + } + + @Override + public void onPipDismiss() { + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_TAP_TO_REMOVE); + mTouchState.removeDoubleTapTimeoutCallback(); + mMotionHelper.dismissPip(); + } + + @Override + public void onPipShowMenu() { + mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), + true /* allowMenuTimeout */, willResizeMenu(), shouldShowResizeHandle()); + } + } + + @SuppressLint("InflateParams") + public PipTouchHandler(Context context, + PipMenuActivityController menuController, + PipBoundsHandler pipBoundsHandler, + @NonNull PipBoundsState pipBoundsState, + PipTaskOrganizer pipTaskOrganizer, + FloatingContentCoordinator floatingContentCoordinator, + PipUiEventLogger pipUiEventLogger) { + // Initialize the Pip input consumer + mContext = context; + mAccessibilityManager = context.getSystemService(AccessibilityManager.class); + mPipBoundsHandler = pipBoundsHandler; + mPipBoundsState = pipBoundsState; + mMenuController = menuController; + mMenuController.addListener(new PipMenuListener()); + mGesture = new DefaultPipTouchGesture(); + mMotionHelper = new PipMotionHelper(mContext, pipBoundsState, pipTaskOrganizer, + mMenuController, mPipBoundsHandler.getSnapAlgorithm(), floatingContentCoordinator); + mPipResizeGestureHandler = + new PipResizeGestureHandler(context, pipBoundsHandler, mMotionHelper, + pipTaskOrganizer, this::getMovementBounds, + this::updateMovementBounds, pipUiEventLogger, menuController); + mPipDismissTargetHandler = new PipDismissTargetHandler(context, pipUiEventLogger, + mMotionHelper, mHandler); + mTouchState = new PipTouchState(ViewConfiguration.get(context), mHandler, + () -> mMenuController.showMenuWithDelay(MENU_STATE_FULL, mMotionHelper.getBounds(), + true /* allowMenuTimeout */, willResizeMenu(), shouldShowResizeHandle()), + menuController::hideMenu); + + Resources res = context.getResources(); + mEnableResize = res.getBoolean(R.bool.config_pipEnableResizeForMenu); + reloadResources(); + + mFloatingContentCoordinator = floatingContentCoordinator; + mConnection = new PipAccessibilityInteractionConnection(mContext, pipBoundsState, + mMotionHelper, pipTaskOrganizer, mPipBoundsHandler.getSnapAlgorithm(), + this::onAccessibilityShowMenu, this::updateMovementBounds, mHandler); + + mPipUiEventLogger = pipUiEventLogger; + + mEnableStash = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_SYSTEMUI, + PIP_STASHING, + /* defaultValue = */ false); + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, + context.getMainExecutor(), + properties -> { + if (properties.getKeyset().contains(PIP_STASHING)) { + mEnableStash = properties.getBoolean( + PIP_STASHING, /* defaultValue = */ false); + } + }); + } + + private void reloadResources() { + final Resources res = mContext.getResources(); + mBottomOffsetBufferPx = res.getDimensionPixelSize(R.dimen.pip_bottom_offset_buffer); + mExpandedShortestEdgeSize = res.getDimensionPixelSize( + R.dimen.pip_expanded_shortest_edge_size); + mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset); + mPipDismissTargetHandler.updateMagneticTargetSize(); + } + + private boolean shouldShowResizeHandle() { + return false; + } + + public void setTouchGesture(PipTouchGesture gesture) { + mGesture = gesture; + } + + public void setTouchEnabled(boolean enabled) { + mTouchState.setAllowTouches(enabled); + } + + public void showPictureInPictureMenu() { + // Only show the menu if the user isn't currently interacting with the PiP + if (!mTouchState.isUserInteracting()) { + mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), + false /* allowMenuTimeout */, willResizeMenu(), + shouldShowResizeHandle()); + } + } + + public void onActivityPinned() { + mPipDismissTargetHandler.createOrUpdateDismissTarget(); + + mShowPipMenuOnAnimationEnd = true; + mPipResizeGestureHandler.onActivityPinned(); + mFloatingContentCoordinator.onContentAdded(mMotionHelper); + } + + public void onActivityUnpinned(ComponentName topPipActivity) { + if (topPipActivity == null) { + // Clean up state after the last PiP activity is removed + mPipDismissTargetHandler.cleanUpDismissTarget(); + + mFloatingContentCoordinator.onContentRemoved(mMotionHelper); + } + mPipResizeGestureHandler.onActivityUnpinned(); + } + + public void onPinnedStackAnimationEnded( + @PipAnimationController.TransitionDirection int direction) { + // Always synchronize the motion helper bounds once PiP animations finish + mMotionHelper.synchronizePinnedStackBounds(); + updateMovementBounds(); + if (direction == TRANSITION_DIRECTION_TO_PIP) { + // Set the initial bounds as the user resize bounds. + mPipResizeGestureHandler.setUserResizeBounds(mMotionHelper.getBounds()); + } + + if (mShowPipMenuOnAnimationEnd) { + mMenuController.showMenu(MENU_STATE_CLOSE, mMotionHelper.getBounds(), + true /* allowMenuTimeout */, false /* willResizeMenu */, + shouldShowResizeHandle()); + mShowPipMenuOnAnimationEnd = false; + } + } + + public void onConfigurationChanged() { + mPipResizeGestureHandler.onConfigurationChanged(); + mMotionHelper.synchronizePinnedStackBounds(); + reloadResources(); + + // Recreate the dismiss target for the new orientation. + mPipDismissTargetHandler.createOrUpdateDismissTarget(); + } + + public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { + mIsImeShowing = imeVisible; + mImeHeight = imeHeight; + } + + public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) { + mIsShelfShowing = shelfVisible; + mShelfHeight = shelfHeight; + } + + /** + * Called when SysUI state changed. + * + * @param isSysUiStateValid Is SysUI valid or not. + */ + public void onSystemUiStateChanged(boolean isSysUiStateValid) { + mPipResizeGestureHandler.onSystemUiStateChanged(isSysUiStateValid); + } + + public void adjustBoundsForRotation(Rect outBounds, Rect curBounds, Rect insetBounds) { + final Rect toMovementBounds = new Rect(); + mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(outBounds, insetBounds, + toMovementBounds, 0); + final int prevBottom = mMovementBounds.bottom - mMovementBoundsExtraOffsets; + if ((prevBottom - mBottomOffsetBufferPx) <= curBounds.top) { + outBounds.offsetTo(outBounds.left, toMovementBounds.bottom); + } + } + + /** + * Responds to IPinnedStackListener on resetting aspect ratio for the pinned window. + */ + public void onAspectRatioChanged() { + mPipResizeGestureHandler.invalidateUserResizeBounds(); + } + + public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect curBounds, + boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation) { + // Set the user resized bounds equal to the new normal bounds in case they were + // invalidated (e.g. by an aspect ratio change). + if (mPipResizeGestureHandler.getUserResizeBounds().isEmpty()) { + mPipResizeGestureHandler.setUserResizeBounds(normalBounds); + } + + final int bottomOffset = mIsImeShowing ? mImeHeight : 0; + final boolean fromDisplayRotationChanged = (mDisplayRotation != displayRotation); + if (fromDisplayRotationChanged) { + mTouchState.reset(); + } + + // Re-calculate the expanded bounds + mNormalBounds.set(normalBounds); + Rect normalMovementBounds = new Rect(); + mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(mNormalBounds, insetBounds, + normalMovementBounds, bottomOffset); + + if (mMovementBounds.isEmpty()) { + // mMovementBounds is not initialized yet and a clean movement bounds without + // bottom offset shall be used later in this function. + mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(curBounds, insetBounds, + mMovementBounds, 0 /* bottomOffset */); + } + + // Calculate the expanded size + float aspectRatio = (float) normalBounds.width() / normalBounds.height(); + Point displaySize = new Point(); + mContext.getDisplay().getRealSize(displaySize); + Size expandedSize = mPipBoundsHandler.getSnapAlgorithm().getSizeForAspectRatio(aspectRatio, + mExpandedShortestEdgeSize, displaySize.x, displaySize.y); + mExpandedBounds.set(0, 0, expandedSize.getWidth(), expandedSize.getHeight()); + Rect expandedMovementBounds = new Rect(); + mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(mExpandedBounds, insetBounds, + expandedMovementBounds, bottomOffset); + + mPipResizeGestureHandler.updateMinSize(mNormalBounds.width(), mNormalBounds.height()); + mPipResizeGestureHandler.updateMaxSize(mExpandedBounds.width(), mExpandedBounds.height()); + + // The extra offset does not really affect the movement bounds, but are applied based on the + // current state (ime showing, or shelf offset) when we need to actually shift + int extraOffset = Math.max( + mIsImeShowing ? mImeOffset : 0, + !mIsImeShowing && mIsShelfShowing ? mShelfHeight : 0); + + // If this is from an IME or shelf adjustment, then we should move the PiP so that it is not + // occluded by the IME or shelf. + if (fromImeAdjustment || fromShelfAdjustment) { + if (mTouchState.isUserInteracting()) { + // Defer the update of the current movement bounds until after the user finishes + // touching the screen + } else { + final boolean isExpanded = mMenuState == MENU_STATE_FULL && willResizeMenu(); + final Rect toMovementBounds = new Rect(); + mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(curBounds, insetBounds, + toMovementBounds, mIsImeShowing ? mImeHeight : 0); + final int prevBottom = mMovementBounds.bottom - mMovementBoundsExtraOffsets; + // This is to handle landscape fullscreen IMEs, don't apply the extra offset in this + // case + final int toBottom = toMovementBounds.bottom < toMovementBounds.top + ? toMovementBounds.bottom + : toMovementBounds.bottom - extraOffset; + + if (isExpanded) { + curBounds.set(mExpandedBounds); + mPipBoundsHandler.getSnapAlgorithm().applySnapFraction(curBounds, + toMovementBounds, mSavedSnapFraction); + } + + if (prevBottom < toBottom) { + // The movement bounds are expanding + if (curBounds.top > prevBottom - mBottomOffsetBufferPx) { + mMotionHelper.animateToOffset(curBounds, toBottom - curBounds.top); + } + } else if (prevBottom > toBottom) { + // The movement bounds are shrinking + if (curBounds.top > toBottom - mBottomOffsetBufferPx) { + mMotionHelper.animateToOffset(curBounds, toBottom - curBounds.top); + } + } + } + } + + // Update the movement bounds after doing the calculations based on the old movement bounds + // above + mNormalMovementBounds.set(normalMovementBounds); + mExpandedMovementBounds.set(expandedMovementBounds); + mDisplayRotation = displayRotation; + mInsetBounds.set(insetBounds); + updateMovementBounds(); + mMovementBoundsExtraOffsets = extraOffset; + mConnection.onMovementBoundsChanged(mNormalBounds, mExpandedBounds, mNormalMovementBounds, + mExpandedMovementBounds); + + // If we have a deferred resize, apply it now + if (mDeferResizeToNormalBoundsUntilRotation == displayRotation) { + mMotionHelper.animateToUnexpandedState(normalBounds, mSavedSnapFraction, + mNormalMovementBounds, mMovementBounds, true /* immediate */); + mSavedSnapFraction = -1f; + mDeferResizeToNormalBoundsUntilRotation = -1; + } + } + + /** + * TODO Add appropriate description + */ + public void onRegistrationChanged(boolean isRegistered) { + mAccessibilityManager.setPictureInPictureActionReplacingConnection(isRegistered + ? mConnection : null); + if (!isRegistered && mTouchState.isUserInteracting()) { + // If the input consumer is unregistered while the user is interacting, then we may not + // get the final TOUCH_UP event, so clean up the dismiss target as well + mPipDismissTargetHandler.cleanUpDismissTarget(); + } + } + + private void onAccessibilityShowMenu() { + mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), + true /* allowMenuTimeout */, willResizeMenu(), + shouldShowResizeHandle()); + } + + /** + * TODO Add appropriate description + */ + public boolean handleTouchEvent(InputEvent inputEvent) { + // Skip any non motion events + if (!(inputEvent instanceof MotionEvent)) { + return true; + } + // Skip touch handling until we are bound to the controller + if (mPinnedStackController == null) { + return true; + } + + MotionEvent ev = (MotionEvent) inputEvent; + if (mPipResizeGestureHandler.willStartResizeGesture(ev)) { + // Initialize the touch state for the gesture, but immediately reset to invalidate the + // gesture + mTouchState.onTouchEvent(ev); + mTouchState.reset(); + return true; + } + + if ((ev.getAction() == MotionEvent.ACTION_DOWN || mTouchState.isUserInteracting()) + && mPipDismissTargetHandler.maybeConsumeMotionEvent(ev)) { + // If the first touch event occurs within the magnetic field, pass the ACTION_DOWN event + // to the touch state. Touch state needs a DOWN event in order to later process MOVE + // events it'll receive if the object is dragged out of the magnetic field. + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mTouchState.onTouchEvent(ev); + } + + // Continue tracking velocity when the object is in the magnetic field, since we want to + // respect touch input velocity if the object is dragged out and then flung. + mTouchState.addMovementToVelocityTracker(ev); + + return true; + } + + // Update the touch state + mTouchState.onTouchEvent(ev); + + boolean shouldDeliverToMenu = mMenuState != MENU_STATE_NONE; + + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: { + mGesture.onDown(mTouchState); + break; + } + case MotionEvent.ACTION_MOVE: { + if (mGesture.onMove(mTouchState)) { + break; + } + + shouldDeliverToMenu = !mTouchState.isDragging(); + break; + } + case MotionEvent.ACTION_UP: { + // Update the movement bounds again if the state has changed since the user started + // dragging (ie. when the IME shows) + updateMovementBounds(); + + if (mGesture.onUp(mTouchState)) { + break; + } + + // Fall through to clean up + } + case MotionEvent.ACTION_CANCEL: { + shouldDeliverToMenu = !mTouchState.startedDragging() && !mTouchState.isDragging(); + mTouchState.reset(); + break; + } + case MotionEvent.ACTION_HOVER_ENTER: + // If Touch Exploration is enabled, some a11y services (e.g. Talkback) is probably + // on and changing MotionEvents into HoverEvents. + // Let's not enable menu show/hide for a11y services. + if (!mAccessibilityManager.isTouchExplorationEnabled()) { + mTouchState.removeHoverExitTimeoutCallback(); + mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), + false /* allowMenuTimeout */, false /* willResizeMenu */, + shouldShowResizeHandle()); + } + case MotionEvent.ACTION_HOVER_MOVE: { + if (!shouldDeliverToMenu && !mSendingHoverAccessibilityEvents) { + sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + mSendingHoverAccessibilityEvents = true; + } + break; + } + case MotionEvent.ACTION_HOVER_EXIT: { + // If Touch Exploration is enabled, some a11y services (e.g. Talkback) is probably + // on and changing MotionEvents into HoverEvents. + // Let's not enable menu show/hide for a11y services. + if (!mAccessibilityManager.isTouchExplorationEnabled()) { + mTouchState.scheduleHoverExitTimeoutCallback(); + } + if (!shouldDeliverToMenu && mSendingHoverAccessibilityEvents) { + sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + mSendingHoverAccessibilityEvents = false; + } + break; + } + } + + // Deliver the event to PipMenuActivity to handle button click if the menu has shown. + if (shouldDeliverToMenu) { + final MotionEvent cloneEvent = MotionEvent.obtain(ev); + // Send the cancel event and cancel menu timeout if it starts to drag. + if (mTouchState.startedDragging()) { + cloneEvent.setAction(MotionEvent.ACTION_CANCEL); + mMenuController.pokeMenu(); + } + + mMenuController.handlePointerEvent(cloneEvent); + } + + return true; + } + + private void sendAccessibilityHoverEvent(int type) { + if (!mAccessibilityManager.isEnabled()) { + return; + } + + AccessibilityEvent event = AccessibilityEvent.obtain(type); + event.setImportantForAccessibility(true); + event.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID); + event.setWindowId( + AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID); + mAccessibilityManager.sendAccessibilityEvent(event); + } + + /** + * Sets the controller to update the system of changes from user interaction. + */ + void setPinnedStackController(IPinnedStackController controller) { + mPinnedStackController = controller; + } + + /** + * Sets the menu visibility. + */ + private void setMenuState(int menuState, boolean resize, Runnable callback) { + if (mMenuState == menuState && !resize) { + return; + } + + if (menuState == MENU_STATE_FULL && mMenuState != MENU_STATE_FULL) { + // Save the current snap fraction and if we do not drag or move the PiP, then + // we store back to this snap fraction. Otherwise, we'll reset the snap + // fraction and snap to the closest edge. + if (resize) { + animateToExpandedState(callback); + } + } else if (menuState == MENU_STATE_NONE && mMenuState == MENU_STATE_FULL) { + // Try and restore the PiP to the closest edge, using the saved snap fraction + // if possible + if (resize) { + if (mDeferResizeToNormalBoundsUntilRotation == -1) { + // This is a very special case: when the menu is expanded and visible, + // navigating to another activity can trigger auto-enter PiP, and if the + // revealed activity has a forced rotation set, then the controller will get + // updated with the new rotation of the display. However, at the same time, + // SystemUI will try to hide the menu by creating an animation to the normal + // bounds which are now stale. In such a case we defer the animation to the + // normal bounds until after the next onMovementBoundsChanged() call to get the + // bounds in the new orientation + try { + int displayRotation = mPinnedStackController.getDisplayRotation(); + if (mDisplayRotation != displayRotation) { + mDeferResizeToNormalBoundsUntilRotation = displayRotation; + } + } catch (RemoteException e) { + Log.e(TAG, "Could not get display rotation from controller"); + } + } + + if (mDeferResizeToNormalBoundsUntilRotation == -1) { + animateToUnexpandedState(getUserResizeBounds()); + } + } else { + mSavedSnapFraction = -1f; + } + } + mMenuState = menuState; + updateMovementBounds(); + // If pip menu has dismissed, we should register the A11y ActionReplacingConnection for pip + // as well, or it can't handle a11y focus and pip menu can't perform any action. + onRegistrationChanged(menuState == MENU_STATE_NONE); + if (menuState == MENU_STATE_NONE) { + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_HIDE_MENU); + } else if (menuState == MENU_STATE_FULL) { + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_SHOW_MENU); + } + } + + private void animateToExpandedState(Runnable callback) { + Rect expandedBounds = new Rect(mExpandedBounds); + mSavedSnapFraction = mMotionHelper.animateToExpandedState(expandedBounds, + mMovementBounds, mExpandedMovementBounds, callback); + } + + private void animateToUnexpandedState(Rect restoreBounds) { + Rect restoredMovementBounds = new Rect(); + mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(restoreBounds, + mInsetBounds, restoredMovementBounds, mIsImeShowing ? mImeHeight : 0); + mMotionHelper.animateToUnexpandedState(restoreBounds, mSavedSnapFraction, + restoredMovementBounds, mMovementBounds, false /* immediate */); + mSavedSnapFraction = -1f; + } + + /** + * @return the motion helper. + */ + public PipMotionHelper getMotionHelper() { + return mMotionHelper; + } + + @VisibleForTesting + public PipResizeGestureHandler getPipResizeGestureHandler() { + return mPipResizeGestureHandler; + } + + @VisibleForTesting + public void setPipResizeGestureHandler(PipResizeGestureHandler pipResizeGestureHandler) { + mPipResizeGestureHandler = pipResizeGestureHandler; + } + + @VisibleForTesting + public void setPipMotionHelper(PipMotionHelper pipMotionHelper) { + mMotionHelper = pipMotionHelper; + } + + /** + * @return the unexpanded bounds. + */ + public Rect getNormalBounds() { + return mNormalBounds; + } + + Rect getUserResizeBounds() { + return mPipResizeGestureHandler.getUserResizeBounds(); + } + + /** + * Gesture controlling normal movement of the PIP. + */ + private class DefaultPipTouchGesture extends PipTouchGesture { + private final Point mStartPosition = new Point(); + private final PointF mDelta = new PointF(); + private boolean mShouldHideMenuAfterFling; + + @Override + public void onDown(PipTouchState touchState) { + if (!touchState.isUserInteracting()) { + return; + } + + Rect bounds = mMotionHelper.getPossiblyAnimatingBounds(); + mDelta.set(0f, 0f); + mStartPosition.set(bounds.left, bounds.top); + mMovementWithinDismiss = touchState.getDownTouchPosition().y >= mMovementBounds.bottom; + mMotionHelper.setSpringingToTouch(false); + + // If the menu is still visible then just poke the menu + // so that it will timeout after the user stops touching it + if (mMenuState != MENU_STATE_NONE) { + mMenuController.pokeMenu(); + } + } + + @Override + public boolean onMove(PipTouchState touchState) { + if (!touchState.isUserInteracting()) { + return false; + } + + if (touchState.startedDragging()) { + mSavedSnapFraction = -1f; + mPipDismissTargetHandler.showDismissTargetMaybe(); + } + + if (touchState.isDragging()) { + // Move the pinned stack freely + final PointF lastDelta = touchState.getLastTouchDelta(); + float lastX = mStartPosition.x + mDelta.x; + float lastY = mStartPosition.y + mDelta.y; + float left = lastX + lastDelta.x; + float top = lastY + lastDelta.y; + + // Add to the cumulative delta after bounding the position + mDelta.x += left - lastX; + mDelta.y += top - lastY; + + mTmpBounds.set(mMotionHelper.getPossiblyAnimatingBounds()); + mTmpBounds.offsetTo((int) left, (int) top); + mMotionHelper.movePip(mTmpBounds, true /* isDragging */); + + final PointF curPos = touchState.getLastTouchPosition(); + if (mMovementWithinDismiss) { + // Track if movement remains near the bottom edge to identify swipe to dismiss + mMovementWithinDismiss = curPos.y >= mMovementBounds.bottom; + } + return true; + } + return false; + } + + @Override + public boolean onUp(PipTouchState touchState) { + mPipDismissTargetHandler.hideDismissTargetMaybe(); + + if (!touchState.isUserInteracting()) { + return false; + } + + final PointF vel = touchState.getVelocity(); + + if (touchState.isDragging()) { + if (mMenuState != MENU_STATE_NONE) { + // If the menu is still visible, then just poke the menu so that + // it will timeout after the user stops touching it + mMenuController.showMenu(mMenuState, mMotionHelper.getBounds(), + true /* allowMenuTimeout */, willResizeMenu(), + shouldShowResizeHandle()); + } + mShouldHideMenuAfterFling = mMenuState == MENU_STATE_NONE; + + // Reset the touch state on up before the fling settles + mTouchState.reset(); + final Rect animatingBounds = mMotionHelper.getPossiblyAnimatingBounds(); + // If User releases the PIP window while it's out of the display bounds, put + // PIP into stashed mode. + if (mEnableStash + && (animatingBounds.right > mPipBoundsHandler.getDisplayBounds().right + || animatingBounds.left < mPipBoundsHandler.getDisplayBounds().left)) { + mMotionHelper.stashToEdge(vel.x, vel.y, this::flingEndAction /* endAction */); + } else { + mMotionHelper.flingToSnapTarget(vel.x, vel.y, + this::flingEndAction /* endAction */); + } + } else if (mTouchState.isDoubleTap()) { + // If using pinch to zoom, double-tap functions as resizing between max/min size + if (mPipResizeGestureHandler.isUsingPinchToZoom()) { + final boolean toExpand = + mMotionHelper.getBounds().width() < mExpandedBounds.width() + && mMotionHelper.getBounds().height() < mExpandedBounds.height(); + mPipResizeGestureHandler.setUserResizeBounds(toExpand ? mExpandedBounds + : mNormalBounds); + if (toExpand) { + animateToExpandedState(null); + } else { + animateToUnexpandedState(mNormalBounds); + } + } else { + // Expand to fullscreen if this is a double tap + // the PiP should be frozen until the transition ends + setTouchEnabled(false); + mMotionHelper.expandLeavePip(); + } + } else if (mMenuState != MENU_STATE_FULL) { + if (!mTouchState.isWaitingForDoubleTap()) { + // User has stalled long enough for this not to be a drag or a double tap, just + // expand the menu + mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), + true /* allowMenuTimeout */, willResizeMenu(), + shouldShowResizeHandle()); + } else { + // Next touch event _may_ be the second tap for the double-tap, schedule a + // fallback runnable to trigger the menu if no touch event occurs before the + // next tap + mTouchState.scheduleDoubleTapTimeoutCallback(); + } + } + return true; + } + + private void flingEndAction() { + if (mShouldHideMenuAfterFling) { + // If the menu is not visible, then we can still be showing the activity for the + // dismiss overlay, so just finish it after the animation completes + mMenuController.hideMenu(); + } + } + } + + /** + * Updates the current movement bounds based on whether the menu is currently visible and + * resized. + */ + private void updateMovementBounds() { + mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(mMotionHelper.getBounds(), + mInsetBounds, mMovementBounds, mIsImeShowing ? mImeHeight : 0); + mMotionHelper.setCurrentMovementBounds(mMovementBounds); + + boolean isMenuExpanded = mMenuState == MENU_STATE_FULL; + mPipBoundsHandler.setMinEdgeSize( + isMenuExpanded && willResizeMenu() ? mExpandedShortestEdgeSize : 0); + } + + private Rect getMovementBounds(Rect curBounds) { + Rect movementBounds = new Rect(); + mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(curBounds, mInsetBounds, + movementBounds, mIsImeShowing ? mImeHeight : 0); + return movementBounds; + } + + /** + * @return whether the menu will resize as a part of showing the full menu. + */ + private boolean willResizeMenu() { + if (!mEnableResize) { + return false; + } + return mExpandedBounds.width() != mNormalBounds.width() + || mExpandedBounds.height() != mNormalBounds.height(); + } + + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mMovementBounds=" + mMovementBounds); + pw.println(innerPrefix + "mNormalBounds=" + mNormalBounds); + pw.println(innerPrefix + "mNormalMovementBounds=" + mNormalMovementBounds); + pw.println(innerPrefix + "mExpandedBounds=" + mExpandedBounds); + pw.println(innerPrefix + "mExpandedMovementBounds=" + mExpandedMovementBounds); + pw.println(innerPrefix + "mMenuState=" + mMenuState); + pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing); + pw.println(innerPrefix + "mImeHeight=" + mImeHeight); + pw.println(innerPrefix + "mIsShelfShowing=" + mIsShelfShowing); + pw.println(innerPrefix + "mShelfHeight=" + mShelfHeight); + pw.println(innerPrefix + "mSavedSnapFraction=" + mSavedSnapFraction); + pw.println(innerPrefix + "mMovementBoundsExtraOffsets=" + mMovementBoundsExtraOffsets); + mPipBoundsHandler.dump(pw, innerPrefix); + mTouchState.dump(pw, innerPrefix); + mMotionHelper.dump(pw, innerPrefix); + if (mPipResizeGestureHandler != null) { + mPipResizeGestureHandler.dump(pw, innerPrefix); + } + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java new file mode 100644 index 000000000000..217150770084 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java @@ -0,0 +1,391 @@ +/* + * 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.pip.phone; + +import android.graphics.PointF; +import android.os.Handler; +import android.util.Log; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.PrintWriter; + +/** + * This keeps track of the touch state throughout the current touch gesture. + */ +public class PipTouchState { + private static final String TAG = "PipTouchState"; + private static final boolean DEBUG = false; + + @VisibleForTesting + public static final long DOUBLE_TAP_TIMEOUT = 200; + static final long HOVER_EXIT_TIMEOUT = 50; + + private final Handler mHandler; + private final ViewConfiguration mViewConfig; + private final Runnable mDoubleTapTimeoutCallback; + private final Runnable mHoverExitTimeoutCallback; + + private VelocityTracker mVelocityTracker; + private long mDownTouchTime = 0; + private long mLastDownTouchTime = 0; + private long mUpTouchTime = 0; + private final PointF mDownTouch = new PointF(); + private final PointF mDownDelta = new PointF(); + private final PointF mLastTouch = new PointF(); + private final PointF mLastDelta = new PointF(); + private final PointF mVelocity = new PointF(); + private boolean mAllowTouches = true; + private boolean mIsUserInteracting = false; + // Set to true only if the multiple taps occur within the double tap timeout + private boolean mIsDoubleTap = false; + // Set to true only if a gesture + private boolean mIsWaitingForDoubleTap = false; + private boolean mIsDragging = false; + // The previous gesture was a drag + private boolean mPreviouslyDragging = false; + private boolean mStartedDragging = false; + private boolean mAllowDraggingOffscreen = false; + private int mActivePointerId; + + public PipTouchState(ViewConfiguration viewConfig, Handler handler, + Runnable doubleTapTimeoutCallback, Runnable hoverExitTimeoutCallback) { + mViewConfig = viewConfig; + mHandler = handler; + mDoubleTapTimeoutCallback = doubleTapTimeoutCallback; + mHoverExitTimeoutCallback = hoverExitTimeoutCallback; + } + + /** + * Resets this state. + */ + public void reset() { + mAllowDraggingOffscreen = false; + mIsDragging = false; + mStartedDragging = false; + mIsUserInteracting = false; + } + + /** + * Processes a given touch event and updates the state. + */ + public void onTouchEvent(MotionEvent ev) { + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + if (!mAllowTouches) { + return; + } + + // Initialize the velocity tracker + initOrResetVelocityTracker(); + addMovementToVelocityTracker(ev); + + mActivePointerId = ev.getPointerId(0); + if (DEBUG) { + Log.e(TAG, "Setting active pointer id on DOWN: " + mActivePointerId); + } + mLastTouch.set(ev.getRawX(), ev.getRawY()); + mDownTouch.set(mLastTouch); + mAllowDraggingOffscreen = true; + mIsUserInteracting = true; + mDownTouchTime = ev.getEventTime(); + mIsDoubleTap = !mPreviouslyDragging + && (mDownTouchTime - mLastDownTouchTime) < DOUBLE_TAP_TIMEOUT; + mIsWaitingForDoubleTap = false; + mIsDragging = false; + mLastDownTouchTime = mDownTouchTime; + if (mDoubleTapTimeoutCallback != null) { + mHandler.removeCallbacks(mDoubleTapTimeoutCallback); + } + break; + } + case MotionEvent.ACTION_MOVE: { + // Skip event if we did not start processing this touch gesture + if (!mIsUserInteracting) { + break; + } + + // Update the velocity tracker + addMovementToVelocityTracker(ev); + int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == -1) { + Log.e(TAG, "Invalid active pointer id on MOVE: " + mActivePointerId); + break; + } + + float x = ev.getRawX(pointerIndex); + float y = ev.getRawY(pointerIndex); + mLastDelta.set(x - mLastTouch.x, y - mLastTouch.y); + mDownDelta.set(x - mDownTouch.x, y - mDownTouch.y); + + boolean hasMovedBeyondTap = mDownDelta.length() > mViewConfig.getScaledTouchSlop(); + if (!mIsDragging) { + if (hasMovedBeyondTap) { + mIsDragging = true; + mStartedDragging = true; + } + } else { + mStartedDragging = false; + } + mLastTouch.set(x, y); + break; + } + case MotionEvent.ACTION_POINTER_UP: { + // Skip event if we did not start processing this touch gesture + if (!mIsUserInteracting) { + break; + } + + // Update the velocity tracker + addMovementToVelocityTracker(ev); + + int pointerIndex = ev.getActionIndex(); + int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // Select a new active pointer id and reset the movement state + final int newPointerIndex = (pointerIndex == 0) ? 1 : 0; + mActivePointerId = ev.getPointerId(newPointerIndex); + if (DEBUG) { + Log.e(TAG, + "Relinquish active pointer id on POINTER_UP: " + mActivePointerId); + } + mLastTouch.set(ev.getRawX(newPointerIndex), ev.getRawY(newPointerIndex)); + } + break; + } + case MotionEvent.ACTION_UP: { + // Skip event if we did not start processing this touch gesture + if (!mIsUserInteracting) { + break; + } + + // Update the velocity tracker + addMovementToVelocityTracker(ev); + mVelocityTracker.computeCurrentVelocity(1000, + mViewConfig.getScaledMaximumFlingVelocity()); + mVelocity.set(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); + + int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == -1) { + Log.e(TAG, "Invalid active pointer id on UP: " + mActivePointerId); + break; + } + + mUpTouchTime = ev.getEventTime(); + mLastTouch.set(ev.getRawX(pointerIndex), ev.getRawY(pointerIndex)); + mPreviouslyDragging = mIsDragging; + mIsWaitingForDoubleTap = !mIsDoubleTap && !mIsDragging + && (mUpTouchTime - mDownTouchTime) < DOUBLE_TAP_TIMEOUT; + + // Fall through to clean up + } + case MotionEvent.ACTION_CANCEL: { + recycleVelocityTracker(); + break; + } + case MotionEvent.ACTION_BUTTON_PRESS: { + removeHoverExitTimeoutCallback(); + break; + } + } + } + + /** + * @return the velocity of the active touch pointer at the point it is lifted off the screen. + */ + public PointF getVelocity() { + return mVelocity; + } + + /** + * @return the last touch position of the active pointer. + */ + public PointF getLastTouchPosition() { + return mLastTouch; + } + + /** + * @return the movement delta between the last handled touch event and the previous touch + * position. + */ + public PointF getLastTouchDelta() { + return mLastDelta; + } + + /** + * @return the down touch position. + */ + public PointF getDownTouchPosition() { + return mDownTouch; + } + + /** + * @return the movement delta between the last handled touch event and the down touch + * position. + */ + public PointF getDownTouchDelta() { + return mDownDelta; + } + + /** + * @return whether the user has started dragging. + */ + public boolean isDragging() { + return mIsDragging; + } + + /** + * @return whether the user is currently interacting with the PiP. + */ + public boolean isUserInteracting() { + return mIsUserInteracting; + } + + /** + * @return whether the user has started dragging just in the last handled touch event. + */ + public boolean startedDragging() { + return mStartedDragging; + } + + /** + * Sets whether touching is currently allowed. + */ + public void setAllowTouches(boolean allowTouches) { + mAllowTouches = allowTouches; + + // If the user happens to touch down before this is sent from the system during a transition + // then block any additional handling by resetting the state now + if (mIsUserInteracting) { + reset(); + } + } + + /** + * Disallows dragging offscreen for the duration of the current gesture. + */ + public void setDisallowDraggingOffscreen() { + mAllowDraggingOffscreen = false; + } + + /** + * @return whether dragging offscreen is allowed during this gesture. + */ + public boolean allowDraggingOffscreen() { + return mAllowDraggingOffscreen; + } + + /** + * @return whether this gesture is a double-tap. + */ + public boolean isDoubleTap() { + return mIsDoubleTap; + } + + /** + * @return whether this gesture will potentially lead to a following double-tap. + */ + public boolean isWaitingForDoubleTap() { + return mIsWaitingForDoubleTap; + } + + /** + * Schedules the callback to run if the next double tap does not occur. Only runs if + * isWaitingForDoubleTap() is true. + */ + public void scheduleDoubleTapTimeoutCallback() { + if (mIsWaitingForDoubleTap) { + long delay = getDoubleTapTimeoutCallbackDelay(); + mHandler.removeCallbacks(mDoubleTapTimeoutCallback); + mHandler.postDelayed(mDoubleTapTimeoutCallback, delay); + } + } + + @VisibleForTesting + public long getDoubleTapTimeoutCallbackDelay() { + if (mIsWaitingForDoubleTap) { + return Math.max(0, DOUBLE_TAP_TIMEOUT - (mUpTouchTime - mDownTouchTime)); + } + return -1; + } + + /** + * Removes the timeout callback if it's in queue. + */ + public void removeDoubleTapTimeoutCallback() { + mIsWaitingForDoubleTap = false; + mHandler.removeCallbacks(mDoubleTapTimeoutCallback); + } + + @VisibleForTesting + public void scheduleHoverExitTimeoutCallback() { + mHandler.removeCallbacks(mHoverExitTimeoutCallback); + mHandler.postDelayed(mHoverExitTimeoutCallback, HOVER_EXIT_TIMEOUT); + } + + void removeHoverExitTimeoutCallback() { + mHandler.removeCallbacks(mHoverExitTimeoutCallback); + } + + void addMovementToVelocityTracker(MotionEvent event) { + if (mVelocityTracker == null) { + return; + } + + // Add movement to velocity tracker using raw screen X and Y coordinates instead + // of window coordinates because the window frame may be moving at the same time. + float deltaX = event.getRawX() - event.getX(); + float deltaY = event.getRawY() - event.getY(); + event.offsetLocation(deltaX, deltaY); + mVelocityTracker.addMovement(event); + event.offsetLocation(-deltaX, -deltaY); + } + + private void initOrResetVelocityTracker() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + } + + private void recycleVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mAllowTouches=" + mAllowTouches); + pw.println(innerPrefix + "mActivePointerId=" + mActivePointerId); + pw.println(innerPrefix + "mDownTouch=" + mDownTouch); + pw.println(innerPrefix + "mDownDelta=" + mDownDelta); + pw.println(innerPrefix + "mLastTouch=" + mLastTouch); + pw.println(innerPrefix + "mLastDelta=" + mLastDelta); + pw.println(innerPrefix + "mVelocity=" + mVelocity); + pw.println(innerPrefix + "mIsUserInteracting=" + mIsUserInteracting); + pw.println(innerPrefix + "mIsDragging=" + mIsDragging); + pw.println(innerPrefix + "mStartedDragging=" + mStartedDragging); + pw.println(innerPrefix + "mAllowDraggingOffscreen=" + mAllowDraggingOffscreen); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipUpdateThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipUpdateThread.java new file mode 100644 index 000000000000..d686cac3457b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipUpdateThread.java @@ -0,0 +1,60 @@ +/* + * 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.pip.phone; + +import android.os.Handler; +import android.os.HandlerThread; + +/** + * Similar to {@link com.android.internal.os.BackgroundThread}, this is a shared singleton + * foreground thread for each process for updating PIP. + */ +public final class PipUpdateThread extends HandlerThread { + private static PipUpdateThread sInstance; + private static Handler sHandler; + + private PipUpdateThread() { + super("pip"); + } + + private static void ensureThreadLocked() { + if (sInstance == null) { + sInstance = new PipUpdateThread(); + sInstance.start(); + sHandler = new Handler(sInstance.getLooper()); + } + } + + /** + * @return the static update thread instance + */ + public static PipUpdateThread get() { + synchronized (PipUpdateThread.class) { + ensureThreadLocked(); + return sInstance; + } + } + /** + * @return the static update thread handler instance + */ + public static Handler getHandler() { + synchronized (PipUpdateThread.class) { + ensureThreadLocked(); + return sHandler; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipUtils.java new file mode 100644 index 000000000000..bd2ba32912bc --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipUtils.java @@ -0,0 +1,59 @@ +/* + * 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.pip.phone; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; + +import android.app.ActivityTaskManager; +import android.app.ActivityTaskManager.RootTaskInfo; +import android.app.IActivityManager; +import android.content.ComponentName; +import android.content.Context; +import android.os.RemoteException; +import android.util.Log; +import android.util.Pair; + +public class PipUtils { + private static final String TAG = "PipUtils"; + + /** + * @return the ComponentName and user id of the top non-SystemUI activity in the pinned stack. + * The component name may be null if no such activity exists. + */ + public static Pair<ComponentName, Integer> getTopPipActivity(Context context, + IActivityManager activityManager) { + try { + final String sysUiPackageName = context.getPackageName(); + final RootTaskInfo pinnedTaskInfo = ActivityTaskManager.getService().getRootTaskInfo( + WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); + if (pinnedTaskInfo != null && pinnedTaskInfo.childTaskIds != null + && pinnedTaskInfo.childTaskIds.length > 0) { + for (int i = pinnedTaskInfo.childTaskNames.length - 1; i >= 0; i--) { + ComponentName cn = ComponentName.unflattenFromString( + pinnedTaskInfo.childTaskNames[i]); + if (cn != null && !cn.getPackageName().equals(sysUiPackageName)) { + return new Pair<>(cn, pinnedTaskInfo.childTaskUserIds[i]); + } + } + } + } catch (RemoteException e) { + Log.w(TAG, "Unable to get pinned stack."); + } + return new Pair<>(null, 0); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlButtonView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlButtonView.java new file mode 100644 index 000000000000..4e82bb557fb9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlButtonView.java @@ -0,0 +1,207 @@ +/* + * 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.pip.tv; + +import android.animation.Animator; +import android.animation.AnimatorInflater; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.android.wm.shell.R; + +/** + * A view containing PIP controls including fullscreen, close, and media controls. + */ +public class PipControlButtonView extends RelativeLayout { + + private OnFocusChangeListener mFocusChangeListener; + private ImageView mIconImageView; + ImageView mButtonImageView; + private TextView mDescriptionTextView; + private Animator mTextFocusGainAnimator; + private Animator mButtonFocusGainAnimator; + private Animator mTextFocusLossAnimator; + private Animator mButtonFocusLossAnimator; + + private final OnFocusChangeListener mInternalFocusChangeListener = + new OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + startFocusGainAnimation(); + } else { + startFocusLossAnimation(); + } + + if (mFocusChangeListener != null) { + mFocusChangeListener.onFocusChange(PipControlButtonView.this, hasFocus); + } + } + }; + + public PipControlButtonView(Context context) { + this(context, null, 0, 0); + } + + public PipControlButtonView(Context context, AttributeSet attrs) { + this(context, attrs, 0, 0); + } + + public PipControlButtonView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public PipControlButtonView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.tv_pip_control_button, this); + + mIconImageView = findViewById(R.id.icon); + mButtonImageView = findViewById(R.id.button); + mDescriptionTextView = findViewById(R.id.desc); + + int[] values = new int[]{android.R.attr.src, android.R.attr.text}; + TypedArray typedArray = context.obtainStyledAttributes(attrs, values, defStyleAttr, + defStyleRes); + + setImageResource(typedArray.getResourceId(0, 0)); + setText(typedArray.getResourceId(1, 0)); + + typedArray.recycle(); + } + + @Override + public void onFinishInflate() { + super.onFinishInflate(); + mButtonImageView.setOnFocusChangeListener(mInternalFocusChangeListener); + + mTextFocusGainAnimator = AnimatorInflater.loadAnimator(getContext(), + R.anim.tv_pip_controls_focus_gain_animation); + mTextFocusGainAnimator.setTarget(mDescriptionTextView); + mButtonFocusGainAnimator = AnimatorInflater.loadAnimator(getContext(), + R.anim.tv_pip_controls_focus_gain_animation); + mButtonFocusGainAnimator.setTarget(mButtonImageView); + + mTextFocusLossAnimator = AnimatorInflater.loadAnimator(getContext(), + R.anim.tv_pip_controls_focus_loss_animation); + mTextFocusLossAnimator.setTarget(mDescriptionTextView); + mButtonFocusLossAnimator = AnimatorInflater.loadAnimator(getContext(), + R.anim.tv_pip_controls_focus_loss_animation); + mButtonFocusLossAnimator.setTarget(mButtonImageView); + } + + @Override + public void setOnClickListener(OnClickListener listener) { + mButtonImageView.setOnClickListener(listener); + } + + @Override + public void setOnFocusChangeListener(OnFocusChangeListener listener) { + mFocusChangeListener = listener; + } + + /** + * Sets the drawable for the button with the given drawable. + */ + public void setImageDrawable(Drawable d) { + mIconImageView.setImageDrawable(d); + } + + /** + * Sets the drawable for the button with the given resource id. + */ + public void setImageResource(int resId) { + if (resId != 0) { + mIconImageView.setImageResource(resId); + } + } + + /** + * Sets the text for description the with the given string. + */ + public void setText(CharSequence text) { + mButtonImageView.setContentDescription(text); + mDescriptionTextView.setText(text); + } + + /** + * Sets the text for description the with the given resource id. + */ + public void setText(int resId) { + if (resId != 0) { + mButtonImageView.setContentDescription(getContext().getString(resId)); + mDescriptionTextView.setText(resId); + } + } + + private static void cancelAnimator(Animator animator) { + if (animator.isStarted()) { + animator.cancel(); + } + } + + /** + * Starts the focus gain animation. + */ + public void startFocusGainAnimation() { + cancelAnimator(mButtonFocusLossAnimator); + cancelAnimator(mTextFocusLossAnimator); + mTextFocusGainAnimator.start(); + if (mButtonImageView.getAlpha() < 1f) { + // If we had faded out the ripple drawable, run our manual focus change animation. + // See the comment at {@link #startFocusLossAnimation()} for the reason of manual + // animator. + mButtonFocusGainAnimator.start(); + } + } + + /** + * Starts the focus loss animation. + */ + public void startFocusLossAnimation() { + cancelAnimator(mButtonFocusGainAnimator); + cancelAnimator(mTextFocusGainAnimator); + mTextFocusLossAnimator.start(); + if (mButtonImageView.hasFocus()) { + // Button uses ripple that has the default animation for the focus changes. + // Howevever, it doesn't expose the API to fade out while it is focused, + // so we should manually run the fade out animation when PIP controls row loses focus. + mButtonFocusLossAnimator.start(); + } + } + + /** + * Resets to initial state. + */ + public void reset() { + cancelAnimator(mButtonFocusGainAnimator); + cancelAnimator(mTextFocusGainAnimator); + cancelAnimator(mButtonFocusLossAnimator); + cancelAnimator(mTextFocusLossAnimator); + mButtonImageView.setAlpha(1f); + mDescriptionTextView.setAlpha(mButtonImageView.hasFocus() ? 1f : 0f); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipController.java new file mode 100644 index 000000000000..4f2d4e50f76d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipController.java @@ -0,0 +1,750 @@ +/* + * 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.pip.tv; + +import static android.app.ActivityTaskManager.INVALID_STACK_ID; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.content.Intent.ACTION_MEDIA_RESOURCE_GRANTED; + +import static com.android.wm.shell.pip.tv.PipNotification.ACTION_CLOSE; +import static com.android.wm.shell.pip.tv.PipNotification.ACTION_MENU; + +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.ActivityTaskManager.RootTaskInfo; +import android.app.IActivityTaskManager; +import android.app.RemoteAction; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ParceledListSlice; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Rect; +import android.media.session.MediaController; +import android.media.session.MediaSessionManager; +import android.media.session.PlaybackState; +import android.os.Debug; +import android.os.Handler; +import android.os.RemoteException; +import android.os.UserHandle; +import android.text.TextUtils; +import android.util.Log; +import android.view.DisplayInfo; + +import com.android.wm.shell.R; +import com.android.wm.shell.WindowManagerShellWrapper; +import com.android.wm.shell.pip.PinnedStackListenerForwarder; +import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.pip.PipBoundsHandler; +import com.android.wm.shell.pip.PipTaskOrganizer; + +import java.util.ArrayList; +import java.util.List; + +/** + * Manages the picture-in-picture (PIP) UI and states. + */ +public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallback { + private static final String TAG = "PipController"; + static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + /** + * Unknown or invalid state + */ + public static final int STATE_UNKNOWN = -1; + /** + * State when there's no PIP. + */ + public static final int STATE_NO_PIP = 0; + /** + * State when PIP is shown. This is used as default PIP state. + */ + public static final int STATE_PIP = 1; + /** + * State when PIP menu dialog is shown. + */ + public static final int STATE_PIP_MENU = 2; + + private static final int TASK_ID_NO_PIP = -1; + private static final int INVALID_RESOURCE_TYPE = -1; + + public static final int SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_MENU_ACTIVITY_FINISH = 0x1; + + /** + * PIPed activity is playing a media and it can be paused. + */ + static final int PLAYBACK_STATE_PLAYING = 0; + /** + * PIPed activity has a paused media and it can be played. + */ + static final int PLAYBACK_STATE_PAUSED = 1; + /** + * Users are unable to control PIPed activity's media playback. + */ + static final int PLAYBACK_STATE_UNAVAILABLE = 2; + + private static final int CLOSE_PIP_WHEN_MEDIA_SESSION_GONE_TIMEOUT_MS = 3000; + + private int mSuspendPipResizingReason; + + private Context mContext; + private PipBoundsHandler mPipBoundsHandler; + private PipTaskOrganizer mPipTaskOrganizer; + private IActivityTaskManager mActivityTaskManager; + private MediaSessionManager mMediaSessionManager; + private int mState = STATE_NO_PIP; + private int mResumeResizePinnedStackRunnableState = STATE_NO_PIP; + private final Handler mHandler = new Handler(); + private List<Listener> mListeners = new ArrayList<>(); + private List<MediaListener> mMediaListeners = new ArrayList<>(); + private Rect mPipBounds; + private Rect mDefaultPipBounds = new Rect(); + private Rect mMenuModePipBounds; + private int mLastOrientation = Configuration.ORIENTATION_UNDEFINED; + private boolean mInitialized; + private int mPipTaskId = TASK_ID_NO_PIP; + private int mPinnedStackId = INVALID_STACK_ID; + private ComponentName mPipComponentName; + private MediaController mPipMediaController; + private String[] mLastPackagesResourceGranted; + private PipNotification mPipNotification; + private ParceledListSlice<RemoteAction> mCustomActions; + private WindowManagerShellWrapper mWindowManagerShellWrapper; + private int mResizeAnimationDuration; + + // Used to calculate the movement bounds + private final DisplayInfo mTmpDisplayInfo = new DisplayInfo(); + private final Rect mTmpInsetBounds = new Rect(); + + // Keeps track of the IME visibility to adjust the PiP when the IME is visible + private boolean mImeVisible; + private int mImeHeightAdjustment; + + private final Runnable mResizePinnedStackRunnable = + () -> resizePinnedStack(mResumeResizePinnedStackRunnableState); + private final Runnable mClosePipRunnable = () -> closePip(); + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (DEBUG) { + Log.d(TAG, "mBroadcastReceiver, action: " + intent.getAction()); + } + switch (intent.getAction()) { + case ACTION_MENU: + showPictureInPictureMenu(); + break; + case ACTION_CLOSE: + closePip(); + break; + case ACTION_MEDIA_RESOURCE_GRANTED: + String[] packageNames = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES); + int resourceType = intent.getIntExtra(Intent.EXTRA_MEDIA_RESOURCE_TYPE, + INVALID_RESOURCE_TYPE); + if (packageNames != null && packageNames.length > 0 + && resourceType == Intent.EXTRA_MEDIA_RESOURCE_TYPE_VIDEO_CODEC) { + handleMediaResourceGranted(packageNames); + } + break; + } + } + }; + private final MediaSessionManager.OnActiveSessionsChangedListener mActiveMediaSessionListener = + controllers -> updateMediaController(controllers); + private final PinnedStackListenerForwarder.PinnedStackListener mPinnedStackListener = + new PipControllerPinnedStackListener(); + + @Override + public void registerSessionListenerForCurrentUser() { + // TODO Need confirm if TV have to re-registers when switch user + mMediaSessionManager.removeOnActiveSessionsChangedListener(mActiveMediaSessionListener); + mMediaSessionManager.addOnActiveSessionsChangedListener(mActiveMediaSessionListener, null, + UserHandle.USER_CURRENT, null); + } + + /** + * Handler for messages from the PIP controller. + */ + private class PipControllerPinnedStackListener extends + PinnedStackListenerForwarder.PinnedStackListener { + @Override + public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { + mHandler.post(() -> { + mPipBoundsHandler.onImeVisibilityChanged(imeVisible, imeHeight); + if (mState == STATE_PIP) { + if (mImeVisible != imeVisible) { + if (imeVisible) { + // Save the IME height adjustment, and offset to not occlude the IME + mPipBounds.offset(0, -imeHeight); + mImeHeightAdjustment = imeHeight; + } else { + // Apply the inverse adjustment when the IME is hidden + mPipBounds.offset(0, mImeHeightAdjustment); + } + mImeVisible = imeVisible; + resizePinnedStack(STATE_PIP); + } + } + }); + } + + @Override + public void onMovementBoundsChanged(boolean fromImeAdjustment) { + mHandler.post(() -> { + // Populate the inset / normal bounds and DisplayInfo from mPipBoundsHandler first. + mPipBoundsHandler.onMovementBoundsChanged(mTmpInsetBounds, mPipBounds, + mDefaultPipBounds, mTmpDisplayInfo); + }); + } + + @Override + public void onActionsChanged(ParceledListSlice<RemoteAction> actions) { + mCustomActions = actions; + mHandler.post(() -> { + for (int i = mListeners.size() - 1; i >= 0; --i) { + mListeners.get(i).onPipMenuActionsChanged(mCustomActions); + } + }); + } + } + + public PipController(Context context, + PipBoundsHandler pipBoundsHandler, + PipTaskOrganizer pipTaskOrganizer, + WindowManagerShellWrapper windowManagerShellWrapper + ) { + if (!mInitialized) { + mInitialized = true; + mContext = context; + mPipNotification = new PipNotification(context, this); + mPipBoundsHandler = pipBoundsHandler; + // Ensure that we have the display info in case we get calls to update the bounds + // before the listener calls back + final DisplayInfo displayInfo = new DisplayInfo(); + context.getDisplay().getDisplayInfo(displayInfo); + mPipBoundsHandler.onDisplayInfoChanged(displayInfo); + + mResizeAnimationDuration = context.getResources() + .getInteger(R.integer.config_pipResizeAnimationDuration); + mPipTaskOrganizer = pipTaskOrganizer; + mPipTaskOrganizer.registerPipTransitionCallback(this); + mActivityTaskManager = ActivityTaskManager.getService(); + + final IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(ACTION_CLOSE); + intentFilter.addAction(ACTION_MENU); + intentFilter.addAction(ACTION_MEDIA_RESOURCE_GRANTED); + mContext.registerReceiver(mBroadcastReceiver, intentFilter, UserHandle.USER_ALL); + + // Initialize the last orientation and apply the current configuration + Configuration initialConfig = mContext.getResources().getConfiguration(); + mLastOrientation = initialConfig.orientation; + loadConfigurationsAndApply(initialConfig); + + mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class); + mWindowManagerShellWrapper = windowManagerShellWrapper; + try { + mWindowManagerShellWrapper.addPinnedStackListener(mPinnedStackListener); + } catch (RemoteException e) { + Log.e(TAG, "Failed to register pinned stack listener", e); + } + + // TODO(b/169395392) Refactor PipMenuActivity to PipMenuView + PipMenuActivity.setPipController(this); + } + } + + private void loadConfigurationsAndApply(Configuration newConfig) { + if (mLastOrientation != newConfig.orientation) { + // Don't resize the pinned stack on orientation change. TV does not care about this case + // and this could clobber the existing animation to the new bounds calculated by WM. + mLastOrientation = newConfig.orientation; + return; + } + + Resources res = mContext.getResources(); + mMenuModePipBounds = Rect.unflattenFromString(res.getString( + R.string.pip_menu_bounds)); + + // Reset the PIP bounds and apply. PIP bounds can be changed by two reasons. + // 1. Configuration changed due to the language change (RTL <-> RTL) + // 2. SystemUI restarts after the crash + mPipBounds = mDefaultPipBounds; + resizePinnedStack(getPinnedTaskInfo() == null ? STATE_NO_PIP : STATE_PIP); + } + + /** + * Updates the PIP per configuration changed. + */ + public void onConfigurationChanged(Configuration newConfig) { + loadConfigurationsAndApply(newConfig); + mPipNotification.onConfigurationChanged(mContext); + } + + /** + * Shows the picture-in-picture menu if an activity is in picture-in-picture mode. + */ + public void showPictureInPictureMenu() { + if (DEBUG) Log.d(TAG, "showPictureInPictureMenu(), current state=" + getStateDescription()); + + if (getState() == STATE_PIP) { + resizePinnedStack(STATE_PIP_MENU); + } + } + + /** + * Closes PIP (PIPed activity and PIP system UI). + */ + public void closePip() { + if (DEBUG) Log.d(TAG, "closePip(), current state=" + getStateDescription()); + + closePipInternal(true); + } + + private void closePipInternal(boolean removePipStack) { + if (DEBUG) { + Log.d(TAG, + "closePipInternal() removePipStack=" + removePipStack + ", current state=" + + getStateDescription()); + } + + mState = STATE_NO_PIP; + mPipTaskId = TASK_ID_NO_PIP; + mPipMediaController = null; + mMediaSessionManager.removeOnActiveSessionsChangedListener(mActiveMediaSessionListener); + if (removePipStack) { + try { + mActivityTaskManager.removeTask(mPinnedStackId); + } catch (RemoteException e) { + Log.e(TAG, "removeTask failed", e); + } finally { + mPinnedStackId = INVALID_STACK_ID; + } + } + for (int i = mListeners.size() - 1; i >= 0; --i) { + mListeners.get(i).onPipActivityClosed(); + } + mHandler.removeCallbacks(mClosePipRunnable); + } + + /** + * Moves the PIPed activity to the fullscreen and closes PIP system UI. + */ + public void movePipToFullscreen() { + if (DEBUG) Log.d(TAG, "movePipToFullscreen(), current state=" + getStateDescription()); + + mPipTaskId = TASK_ID_NO_PIP; + for (int i = mListeners.size() - 1; i >= 0; --i) { + mListeners.get(i).onMoveToFullscreen(); + } + resizePinnedStack(STATE_NO_PIP); + } + + @Override + public void onActivityPinned(String packageName) { + if (DEBUG) Log.d(TAG, "onActivityPinned()"); + + RootTaskInfo taskInfo = getPinnedTaskInfo(); + if (taskInfo == null) { + Log.w(TAG, "Cannot find pinned stack"); + return; + } + if (DEBUG) Log.d(TAG, "PINNED_STACK:" + taskInfo); + mPinnedStackId = taskInfo.taskId; + mPipTaskId = taskInfo.childTaskIds[taskInfo.childTaskIds.length - 1]; + mPipComponentName = ComponentName.unflattenFromString( + taskInfo.childTaskNames[taskInfo.childTaskNames.length - 1]); + // Set state to STATE_PIP so we show it when the pinned stack animation ends. + mState = STATE_PIP; + mMediaSessionManager.addOnActiveSessionsChangedListener( + mActiveMediaSessionListener, null); + updateMediaController(mMediaSessionManager.getActiveSessions(null)); + for (int i = mListeners.size() - 1; i >= 0; i--) { + mListeners.get(i).onPipEntered(packageName); + } + } + + @Override + public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, + boolean clearedTask) { + if (task.configuration.windowConfiguration.getWindowingMode() + != WINDOWING_MODE_PINNED) { + return; + } + if (DEBUG) Log.d(TAG, "onPinnedActivityRestartAttempt()"); + + // If PIPed activity is launched again by Launcher or intent, make it fullscreen. + movePipToFullscreen(); + } + + @Override + public void onTaskStackChanged() { + if (DEBUG) Log.d(TAG, "onTaskStackChanged()"); + + if (getState() != STATE_NO_PIP) { + boolean hasPip = false; + + RootTaskInfo taskInfo = getPinnedTaskInfo(); + if (taskInfo == null || taskInfo.childTaskIds == null) { + Log.w(TAG, "There is nothing in pinned stack"); + closePipInternal(false); + return; + } + for (int i = taskInfo.childTaskIds.length - 1; i >= 0; --i) { + if (taskInfo.childTaskIds[i] == mPipTaskId) { + // PIP task is still alive. + hasPip = true; + break; + } + } + if (!hasPip) { + // PIP task doesn't exist anymore in PINNED_STACK. + closePipInternal(true); + return; + } + } + if (getState() == STATE_PIP) { + if (mPipBounds != mDefaultPipBounds) { + mPipBounds = mDefaultPipBounds; + resizePinnedStack(STATE_PIP); + } + } + } + + /** + * Suspends resizing operation on the Pip until {@link #resumePipResizing} is called + * + * @param reason The reason for suspending resizing operations on the Pip. + */ + public void suspendPipResizing(int reason) { + if (DEBUG) { + Log.d(TAG, + "suspendPipResizing() reason=" + reason + " callers=" + Debug.getCallers(2)); + } + + mSuspendPipResizingReason |= reason; + } + + /** + * Resumes resizing operation on the Pip that was previously suspended. + * + * @param reason The reason resizing operations on the Pip was suspended. + */ + public void resumePipResizing(int reason) { + if ((mSuspendPipResizingReason & reason) == 0) { + return; + } + if (DEBUG) { + Log.d(TAG, + "resumePipResizing() reason=" + reason + " callers=" + Debug.getCallers(2)); + } + mSuspendPipResizingReason &= ~reason; + mHandler.post(mResizePinnedStackRunnable); + } + + /** + * Resize the Pip to the appropriate size for the input state. + * + * @param state In Pip state also used to determine the new size for the Pip. + */ + public void resizePinnedStack(int state) { + if (DEBUG) { + Log.d(TAG, "resizePinnedStack() state=" + stateToName(state) + ", current state=" + + getStateDescription(), new Exception()); + } + + boolean wasStateNoPip = (mState == STATE_NO_PIP); + for (int i = mListeners.size() - 1; i >= 0; --i) { + mListeners.get(i).onPipResizeAboutToStart(); + } + if (mSuspendPipResizingReason != 0) { + mResumeResizePinnedStackRunnableState = state; + if (DEBUG) { + Log.d(TAG, "resizePinnedStack() deferring" + + " mSuspendPipResizingReason=" + mSuspendPipResizingReason + + " mResumeResizePinnedStackRunnableState=" + + stateToName(mResumeResizePinnedStackRunnableState)); + } + return; + } + mState = state; + final Rect newBounds; + switch (mState) { + case STATE_NO_PIP: + newBounds = null; + // If the state was already STATE_NO_PIP, then do not resize the stack below as it + // will not exist + if (wasStateNoPip) { + return; + } + break; + case STATE_PIP_MENU: + newBounds = mMenuModePipBounds; + break; + case STATE_PIP: // fallthrough + default: + newBounds = mPipBounds; + break; + } + if (newBounds != null) { + mPipTaskOrganizer.scheduleAnimateResizePip(newBounds, mResizeAnimationDuration, null); + } else { + mPipTaskOrganizer.exitPip(mResizeAnimationDuration); + } + } + + /** + * @return the current state, or the pending state if the state change was previously suspended. + */ + private int getState() { + if (mSuspendPipResizingReason != 0) { + return mResumeResizePinnedStackRunnableState; + } + return mState; + } + + /** + * Shows PIP menu UI by launching {@link PipMenuActivity}. It also locates the pinned + * stack to the centered PIP bound {@link R.config_centeredPictureInPictureBounds}. + */ + private void showPipMenu() { + if (DEBUG) Log.d(TAG, "showPipMenu(), current state=" + getStateDescription()); + + mState = STATE_PIP_MENU; + for (int i = mListeners.size() - 1; i >= 0; --i) { + mListeners.get(i).onShowPipMenu(); + } + Intent intent = new Intent(mContext, PipMenuActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(PipMenuActivity.EXTRA_CUSTOM_ACTIONS, mCustomActions); + mContext.startActivity(intent); + } + + /** + * Adds a {@link Listener} to PipController. + */ + public void addListener(Listener listener) { + mListeners.add(listener); + } + + /** + * Removes a {@link Listener} from PipController. + */ + public void removeListener(Listener listener) { + mListeners.remove(listener); + } + + /** + * Adds a {@link MediaListener} to PipController. + */ + public void addMediaListener(MediaListener listener) { + mMediaListeners.add(listener); + } + + /** + * Removes a {@link MediaListener} from PipController. + */ + public void removeMediaListener(MediaListener listener) { + mMediaListeners.remove(listener); + } + + /** + * Returns {@code true} if PIP is shown. + */ + public boolean isPipShown() { + return mState != STATE_NO_PIP; + } + + private RootTaskInfo getPinnedTaskInfo() { + RootTaskInfo taskInfo = null; + try { + taskInfo = ActivityTaskManager.getService().getRootTaskInfo( + WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); + } catch (RemoteException e) { + Log.e(TAG, "getRootTaskInfo failed", e); + } + if (DEBUG) Log.d(TAG, "getPinnedTaskInfo(), taskInfo=" + taskInfo); + return taskInfo; + } + + private void handleMediaResourceGranted(String[] packageNames) { + if (getState() == STATE_NO_PIP) { + mLastPackagesResourceGranted = packageNames; + } else { + boolean requestedFromLastPackages = false; + if (mLastPackagesResourceGranted != null) { + for (String packageName : mLastPackagesResourceGranted) { + for (String newPackageName : packageNames) { + if (TextUtils.equals(newPackageName, packageName)) { + requestedFromLastPackages = true; + break; + } + } + } + } + mLastPackagesResourceGranted = packageNames; + if (!requestedFromLastPackages) { + closePip(); + } + } + } + + private void updateMediaController(List<MediaController> controllers) { + MediaController mediaController = null; + if (controllers != null && getState() != STATE_NO_PIP && mPipComponentName != null) { + for (int i = controllers.size() - 1; i >= 0; i--) { + MediaController controller = controllers.get(i); + // We assumes that an app with PIPable activity + // keeps the single instance of media controller especially when PIP is on. + if (controller.getPackageName().equals(mPipComponentName.getPackageName())) { + mediaController = controller; + break; + } + } + } + if (mPipMediaController != mediaController) { + mPipMediaController = mediaController; + for (int i = mMediaListeners.size() - 1; i >= 0; i--) { + mMediaListeners.get(i).onMediaControllerChanged(); + } + if (mPipMediaController == null) { + mHandler.postDelayed(mClosePipRunnable, + CLOSE_PIP_WHEN_MEDIA_SESSION_GONE_TIMEOUT_MS); + } else { + mHandler.removeCallbacks(mClosePipRunnable); + } + } + } + + /** + * Gets the {@link android.media.session.MediaController} for the PIPed activity. + */ + public MediaController getMediaController() { + return mPipMediaController; + } + + @Override + public void hidePipMenu(Runnable onStartCallback, Runnable onEndCallback) { + + } + + /** + * Returns the PIPed activity's playback state. + * This returns one of {@link #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED}, + * or {@link #PLAYBACK_STATE_UNAVAILABLE}. + */ + public int getPlaybackState() { + if (mPipMediaController == null || mPipMediaController.getPlaybackState() == null) { + return PLAYBACK_STATE_UNAVAILABLE; + } + int state = mPipMediaController.getPlaybackState().getState(); + boolean isPlaying = (state == PlaybackState.STATE_BUFFERING + || state == PlaybackState.STATE_CONNECTING + || state == PlaybackState.STATE_PLAYING + || state == PlaybackState.STATE_FAST_FORWARDING + || state == PlaybackState.STATE_REWINDING + || state == PlaybackState.STATE_SKIPPING_TO_PREVIOUS + || state == PlaybackState.STATE_SKIPPING_TO_NEXT); + long actions = mPipMediaController.getPlaybackState().getActions(); + if (!isPlaying && ((actions & PlaybackState.ACTION_PLAY) != 0)) { + return PLAYBACK_STATE_PAUSED; + } else if (isPlaying && ((actions & PlaybackState.ACTION_PAUSE) != 0)) { + return PLAYBACK_STATE_PLAYING; + } + return PLAYBACK_STATE_UNAVAILABLE; + } + + @Override + public void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds) { + } + + @Override + public void onPipTransitionFinished(ComponentName activity, int direction) { + onPipTransitionFinishedOrCanceled(); + } + + @Override + public void onPipTransitionCanceled(ComponentName activity, int direction) { + onPipTransitionFinishedOrCanceled(); + } + + private void onPipTransitionFinishedOrCanceled() { + if (DEBUG) Log.d(TAG, "onPipTransitionFinishedOrCanceled()"); + + if (getState() == STATE_PIP_MENU) { + showPipMenu(); + } + } + + /** + * A listener interface to receive notification on changes in PIP. + */ + public interface Listener { + /** + * Invoked when an activity is pinned and PIP manager is set corresponding information. + * Classes must use this instead of {@link android.app.ITaskStackListener.onActivityPinned} + * because there's no guarantee for the PIP manager be return relavent information + * correctly. (e.g. {@link Pip.isPipShown}). + */ + void onPipEntered(String packageName); + /** Invoked when a PIPed activity is closed. */ + void onPipActivityClosed(); + /** Invoked when the PIP menu gets shown. */ + void onShowPipMenu(); + /** Invoked when the PIP menu actions change. */ + void onPipMenuActionsChanged(ParceledListSlice<RemoteAction> actions); + /** Invoked when the PIPed activity is about to return back to the fullscreen. */ + void onMoveToFullscreen(); + /** Invoked when we are above to start resizing the Pip. */ + void onPipResizeAboutToStart(); + } + + /** + * A listener interface to receive change in PIP's media controller + */ + public interface MediaListener { + /** Invoked when the MediaController on PIPed activity is changed. */ + void onMediaControllerChanged(); + } + + private String getStateDescription() { + if (mSuspendPipResizingReason == 0) { + return stateToName(mState); + } + return stateToName(mResumeResizePinnedStackRunnableState) + " (while " + stateToName(mState) + + " is suspended)"; + } + + private static String stateToName(int state) { + switch (state) { + case STATE_NO_PIP: + return "NO_PIP"; + + case STATE_PIP: + return "PIP"; + + case STATE_PIP_MENU: + return "PIP_MENU"; + + default: + return "UNKNOWN(" + state + ")"; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlsView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlsView.java new file mode 100644 index 000000000000..14960c38fd43 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlsView.java @@ -0,0 +1,65 @@ +/* + * 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.pip.tv; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.widget.LinearLayout; + +import com.android.wm.shell.R; + + +/** + * A view containing PIP controls including fullscreen, close, and media controls. + */ +public class PipControlsView extends LinearLayout { + + public PipControlsView(Context context) { + this(context, null); + } + + public PipControlsView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PipControlsView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public PipControlsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + LayoutInflater layoutInflater = (LayoutInflater) getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + layoutInflater.inflate(R.layout.tv_pip_controls, this); + setOrientation(LinearLayout.HORIZONTAL); + setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL); + } + + PipControlButtonView getFullButtonView() { + return findViewById(R.id.full_button); + } + + PipControlButtonView getCloseButtonView() { + return findViewById(R.id.close_button); + } + + PipControlButtonView getPlayPauseButtonView() { + return findViewById(R.id.play_pause_button); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlsViewController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlsViewController.java new file mode 100644 index 000000000000..f66e9025a9ed --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlsViewController.java @@ -0,0 +1,254 @@ +/* + * 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.pip.tv; + +import android.app.PendingIntent; +import android.app.RemoteAction; +import android.graphics.Color; +import android.media.session.MediaController; +import android.media.session.PlaybackState; +import android.os.Handler; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; + +import com.android.wm.shell.R; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + + +/** + * Controller for {@link PipControlsView}. + */ +public class PipControlsViewController { + private static final String TAG = PipControlsViewController.class.getSimpleName(); + + private static final float DISABLED_ACTION_ALPHA = 0.54f; + + private final PipControlsView mView; + private final LayoutInflater mLayoutInflater; + private final Handler mHandler; + private final PipController mPipController; + private final PipControlButtonView mPlayPauseButtonView; + private MediaController mMediaController; + private PipControlButtonView mFocusedChild; + private Listener mListener; + private ArrayList<PipControlButtonView> mCustomButtonViews = new ArrayList<>(); + private List<RemoteAction> mCustomActions = new ArrayList<>(); + + public PipControlsView getView() { + return mView; + } + + /** + * An interface to listen user action. + */ + public interface Listener { + /** + * Called when a user clicks close PIP button. + */ + void onClosed(); + } + + private View.OnAttachStateChangeListener + mOnAttachStateChangeListener = + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + updateMediaController(); + mPipController.addMediaListener(mPipMediaListener); + } + + @Override + public void onViewDetachedFromWindow(View v) { + mPipController.removeMediaListener(mPipMediaListener); + } + }; + + private MediaController.Callback mMediaControllerCallback = new MediaController.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackState state) { + updateUserActions(); + } + }; + + private final PipController.MediaListener mPipMediaListener = this::updateMediaController; + + private final View.OnFocusChangeListener + mFocusChangeListener = + new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View view, boolean hasFocus) { + if (hasFocus) { + mFocusedChild = (PipControlButtonView) view; + } else if (mFocusedChild == view) { + mFocusedChild = null; + } + } + }; + + public PipControlsViewController(PipControlsView view, PipController pipController, + LayoutInflater layoutInflater, Handler handler) { + super(); + mView = view; + mPipController = pipController; + mLayoutInflater = layoutInflater; + mHandler = handler; + + mView.addOnAttachStateChangeListener(mOnAttachStateChangeListener); + if (mView.isAttachedToWindow()) { + mOnAttachStateChangeListener.onViewAttachedToWindow(mView); + } + + View fullButtonView = mView.getFullButtonView(); + fullButtonView.setOnFocusChangeListener(mFocusChangeListener); + fullButtonView.setOnClickListener(mView -> mPipController.movePipToFullscreen()); + + View closeButtonView = mView.getCloseButtonView(); + closeButtonView.setOnFocusChangeListener(mFocusChangeListener); + closeButtonView.setOnClickListener(v -> { + mPipController.closePip(); + if (mListener != null) { + mListener.onClosed(); + } + }); + + mPlayPauseButtonView = mView.getPlayPauseButtonView(); + mPlayPauseButtonView.setOnFocusChangeListener(mFocusChangeListener); + mPlayPauseButtonView.setOnClickListener(v -> { + if (mMediaController == null || mMediaController.getPlaybackState() == null) { + return; + } + final int playbackState = mPipController.getPlaybackState(); + if (playbackState == PipController.PLAYBACK_STATE_PAUSED) { + mMediaController.getTransportControls().play(); + } else if (playbackState == PipController.PLAYBACK_STATE_PLAYING) { + mMediaController.getTransportControls().pause(); + } + + // View will be updated later in {@link mMediaControllerCallback} + }); + } + + private void updateMediaController() { + AtomicReference<MediaController> newController = new AtomicReference<>(); + newController.set(mPipController.getMediaController()); + + if (newController.get() == null || mMediaController == newController.get()) { + return; + } + if (mMediaController != null) { + mMediaController.unregisterCallback(mMediaControllerCallback); + } + mMediaController = newController.get(); + if (mMediaController != null) { + mMediaController.registerCallback(mMediaControllerCallback); + } + updateUserActions(); + } + + /** + * Updates the actions for the PIP. If there are no custom actions, then the media session + * actions are shown. + */ + private void updateUserActions() { + if (!mCustomActions.isEmpty()) { + // Ensure we have as many buttons as actions + while (mCustomButtonViews.size() < mCustomActions.size()) { + PipControlButtonView buttonView = (PipControlButtonView) mLayoutInflater.inflate( + R.layout.tv_pip_custom_control, mView, false); + mView.addView(buttonView); + mCustomButtonViews.add(buttonView); + } + + // Update the visibility of all views + for (int i = 0; i < mCustomButtonViews.size(); i++) { + mCustomButtonViews.get(i).setVisibility( + i < mCustomActions.size() ? View.VISIBLE : View.GONE); + } + + // Update the state and visibility of the action buttons, and hide the rest + for (int i = 0; i < mCustomActions.size(); i++) { + final RemoteAction action = mCustomActions.get(i); + PipControlButtonView actionView = mCustomButtonViews.get(i); + + // TODO: Check if the action drawable has changed before we reload it + action.getIcon().loadDrawableAsync(mView.getContext(), d -> { + d.setTint(Color.WHITE); + actionView.setImageDrawable(d); + }, mHandler); + actionView.setText(action.getContentDescription()); + if (action.isEnabled()) { + actionView.setOnClickListener(v -> { + try { + action.getActionIntent().send(); + } catch (PendingIntent.CanceledException e) { + Log.w(TAG, "Failed to send action", e); + } + }); + } + actionView.setEnabled(action.isEnabled()); + actionView.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA); + } + + // Hide the media session buttons + mPlayPauseButtonView.setVisibility(View.GONE); + } else { + AtomicInteger state = new AtomicInteger(PipController.STATE_UNKNOWN); + state.set(mPipController.getPlaybackState()); + if (state.get() == PipController.STATE_UNKNOWN + || state.get() == PipController.PLAYBACK_STATE_UNAVAILABLE) { + mPlayPauseButtonView.setVisibility(View.GONE); + } else { + mPlayPauseButtonView.setVisibility(View.VISIBLE); + if (state.get() == PipController.PLAYBACK_STATE_PLAYING) { + mPlayPauseButtonView.setImageResource(R.drawable.pip_ic_pause_white); + mPlayPauseButtonView.setText(R.string.pip_pause); + } else { + mPlayPauseButtonView.setImageResource(R.drawable.pip_ic_play_arrow_white); + mPlayPauseButtonView.setText(R.string.pip_play); + } + } + + // Hide all the custom action buttons + for (int i = 0; i < mCustomButtonViews.size(); i++) { + mCustomButtonViews.get(i).setVisibility(View.GONE); + } + } + } + + + /** + * Sets the {@link Listener} to listen user actions. + */ + public void setListener(Listener listener) { + mListener = listener; + } + + + /** + * Updates the set of activity-defined actions. + */ + public void setActions(List<? extends RemoteAction> actions) { + mCustomActions.clear(); + mCustomActions.addAll(actions); + updateUserActions(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipMenuActivity.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipMenuActivity.java new file mode 100644 index 000000000000..e185a9604449 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipMenuActivity.java @@ -0,0 +1,190 @@ +/* + * 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.pip.tv; + +import android.animation.Animator; +import android.animation.AnimatorInflater; +import android.app.Activity; +import android.app.RemoteAction; +import android.content.Intent; +import android.content.pm.ParceledListSlice; +import android.os.Bundle; +import android.util.Log; + +import com.android.wm.shell.R; + +import java.util.Collections; + +/** + * Activity to show the PIP menu to control PIP. + * TODO(b/169395392) Refactor PipMenuActivity to PipMenuView + */ +public class PipMenuActivity extends Activity implements PipController.Listener { + private static final String TAG = "PipMenuActivity"; + private static final boolean DEBUG = PipController.DEBUG; + + static final String EXTRA_CUSTOM_ACTIONS = "custom_actions"; + + private static PipController sPipController; + + private Animator mFadeInAnimation; + private Animator mFadeOutAnimation; + private boolean mRestorePipSizeWhenClose; + private PipControlsViewController mPipControlsViewController; + + @Override + protected void onCreate(Bundle bundle) { + if (DEBUG) Log.d(TAG, "onCreate()"); + + super.onCreate(bundle); + if (sPipController == null) { + finish(); + } + setContentView(R.layout.tv_pip_menu); + mPipControlsViewController = new PipControlsViewController( + findViewById(R.id.pip_controls), sPipController, + getLayoutInflater(), getApplicationContext().getMainThreadHandler()); + sPipController.addListener(this); + mRestorePipSizeWhenClose = true; + mFadeInAnimation = AnimatorInflater.loadAnimator( + this, R.anim.tv_pip_menu_fade_in_animation); + mFadeInAnimation.setTarget(mPipControlsViewController.getView()); + mFadeOutAnimation = AnimatorInflater.loadAnimator( + this, R.anim.tv_pip_menu_fade_out_animation); + mFadeOutAnimation.setTarget(mPipControlsViewController.getView()); + + onPipMenuActionsChanged(getIntent().getParcelableExtra(EXTRA_CUSTOM_ACTIONS)); + } + + @Override + protected void onNewIntent(Intent intent) { + if (DEBUG) Log.d(TAG, "onNewIntent(), intent=" + intent); + super.onNewIntent(intent); + + onPipMenuActionsChanged(getIntent().getParcelableExtra(EXTRA_CUSTOM_ACTIONS)); + } + + private void restorePipAndFinish() { + if (DEBUG) Log.d(TAG, "restorePipAndFinish()"); + + if (mRestorePipSizeWhenClose) { + if (DEBUG) Log.d(TAG, " > restoring to the default position"); + + // When PIP menu activity is closed, restore to the default position. + sPipController.resizePinnedStack(PipController.STATE_PIP); + } + finish(); + } + + @Override + public void onResume() { + if (DEBUG) Log.d(TAG, "onResume()"); + + super.onResume(); + mFadeInAnimation.start(); + } + + @Override + public void onPause() { + if (DEBUG) Log.d(TAG, "onPause()"); + + super.onPause(); + mFadeOutAnimation.start(); + restorePipAndFinish(); + } + + @Override + protected void onDestroy() { + if (DEBUG) Log.d(TAG, "onDestroy()"); + + super.onDestroy(); + sPipController.removeListener(this); + sPipController.resumePipResizing( + PipController.SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_MENU_ACTIVITY_FINISH); + } + + @Override + public void onBackPressed() { + if (DEBUG) Log.d(TAG, "onBackPressed()"); + + restorePipAndFinish(); + } + + @Override + public void onPipEntered(String packageName) { + if (DEBUG) Log.d(TAG, "onPipEntered(), packageName=" + packageName); + } + + @Override + public void onPipActivityClosed() { + if (DEBUG) Log.d(TAG, "onPipActivityClosed()"); + + finish(); + } + + @Override + public void onPipMenuActionsChanged(ParceledListSlice<RemoteAction> actions) { + if (DEBUG) Log.d(TAG, "onPipMenuActionsChanged()"); + + boolean hasCustomActions = actions != null && !actions.getList().isEmpty(); + mPipControlsViewController.setActions( + hasCustomActions ? actions.getList() : Collections.emptyList()); + } + + @Override + public void onShowPipMenu() { + if (DEBUG) Log.d(TAG, "onShowPipMenu()"); + } + + @Override + public void onMoveToFullscreen() { + if (DEBUG) Log.d(TAG, "onMoveToFullscreen()"); + + // Moving PIP to fullscreen is implemented by resizing PINNED_STACK with null bounds. + // This conflicts with restoring PIP position, so disable it. + mRestorePipSizeWhenClose = false; + finish(); + } + + @Override + public void onPipResizeAboutToStart() { + if (DEBUG) Log.d(TAG, "onPipResizeAboutToStart()"); + + finish(); + sPipController.suspendPipResizing( + PipController.SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_MENU_ACTIVITY_FINISH); + } + + @Override + public void finish() { + if (DEBUG) Log.d(TAG, "finish()", new RuntimeException()); + + super.finish(); + } + + /** + * TODO(b/169395392) Refactor PipMenuActivity to PipMenuView + * + * @param pipController The singleton pipController instance for TV + */ + public static void setPipController(PipController pipController) { + if (sPipController != null) { + return; + } + sPipController = pipController; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipNotification.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipNotification.java new file mode 100644 index 000000000000..f5bbd23fa1d6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipNotification.java @@ -0,0 +1,253 @@ +/* + * 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.pip.tv; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.RemoteAction; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ParceledListSlice; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.media.MediaMetadata; +import android.media.session.MediaController; +import android.media.session.PlaybackState; +import android.text.TextUtils; + +import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; +import com.android.wm.shell.R; + +/** + * A notification that informs users that PIP is running and also provides PIP controls. + * <p>Once it's created, it will manage the PIP notification UI by itself except for handling + * configuration changes. + */ +public class PipNotification { + private static final String TAG = "PipNotification"; + private static final String NOTIFICATION_TAG = PipNotification.class.getSimpleName(); + private static final boolean DEBUG = PipController.DEBUG; + + static final String ACTION_MENU = "PipNotification.menu"; + static final String ACTION_CLOSE = "PipNotification.close"; + + public static final String NOTIFICATION_CHANNEL_TVPIP = "TPP"; + + private final PackageManager mPackageManager; + + private final PipController mPipController; + + private final NotificationManager mNotificationManager; + private final Notification.Builder mNotificationBuilder; + + private MediaController mMediaController; + private String mDefaultTitle; + private int mDefaultIconResId; + + /** Package name for the application that owns PiP window. */ + private String mPackageName; + private boolean mNotified; + private String mMediaTitle; + private Bitmap mArt; + + private PipController.Listener mPipListener = new PipController.Listener() { + @Override + public void onPipEntered(String packageName) { + mPackageName = packageName; + updateMediaControllerMetadata(); + notifyPipNotification(); + } + + @Override + public void onPipActivityClosed() { + dismissPipNotification(); + mPackageName = null; + } + + @Override + public void onShowPipMenu() { + // no-op. + } + + @Override + public void onPipMenuActionsChanged(ParceledListSlice<RemoteAction> actions) { + // no-op. + } + + @Override + public void onMoveToFullscreen() { + dismissPipNotification(); + mPackageName = null; + } + + @Override + public void onPipResizeAboutToStart() { + // no-op. + } + }; + + private MediaController.Callback mMediaControllerCallback = new MediaController.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackState state) { + if (updateMediaControllerMetadata() && mNotified) { + // update notification + notifyPipNotification(); + } + } + + @Override + public void onMetadataChanged(MediaMetadata metadata) { + if (updateMediaControllerMetadata() && mNotified) { + // update notification + notifyPipNotification(); + } + } + }; + + private final PipController.MediaListener mPipMediaListener = + new PipController.MediaListener() { + @Override + public void onMediaControllerChanged() { + MediaController newController = mPipController.getMediaController(); + if (newController == null || mMediaController == newController) { + return; + } + if (mMediaController != null) { + mMediaController.unregisterCallback(mMediaControllerCallback); + } + mMediaController = newController; + if (mMediaController != null) { + mMediaController.registerCallback(mMediaControllerCallback); + } + if (updateMediaControllerMetadata() && mNotified) { + // update notification + notifyPipNotification(); + } + } + }; + + public PipNotification(Context context, PipController pipController) { + mPackageManager = context.getPackageManager(); + + mNotificationManager = (NotificationManager) context.getSystemService( + Context.NOTIFICATION_SERVICE); + + mNotificationBuilder = new Notification.Builder(context, NOTIFICATION_CHANNEL_TVPIP) + .setLocalOnly(true) + .setOngoing(false) + .setCategory(Notification.CATEGORY_SYSTEM) + .extend(new Notification.TvExtender() + .setContentIntent(createPendingIntent(context, ACTION_MENU)) + .setDeleteIntent(createPendingIntent(context, ACTION_CLOSE))); + + mPipController = pipController; + pipController.addListener(mPipListener); + pipController.addMediaListener(mPipMediaListener); + + onConfigurationChanged(context); + } + + /** + * Called by {@link PipController} when the configuration is changed. + */ + void onConfigurationChanged(Context context) { + Resources res = context.getResources(); + mDefaultTitle = res.getString(R.string.pip_notification_unknown_title); + mDefaultIconResId = R.drawable.pip_icon; + if (mNotified) { + // update notification + notifyPipNotification(); + } + } + + private void notifyPipNotification() { + mNotified = true; + mNotificationBuilder + .setShowWhen(true) + .setWhen(System.currentTimeMillis()) + .setSmallIcon(mDefaultIconResId) + .setContentTitle(getNotificationTitle()); + if (mArt != null) { + mNotificationBuilder.setStyle(new Notification.BigPictureStyle() + .bigPicture(mArt)); + } else { + mNotificationBuilder.setStyle(null); + } + mNotificationManager.notify(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP, + mNotificationBuilder.build()); + } + + private void dismissPipNotification() { + mNotified = false; + mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP); + } + + private boolean updateMediaControllerMetadata() { + String title = null; + Bitmap art = null; + if (mPipController.getMediaController() != null) { + MediaMetadata metadata = mPipController.getMediaController().getMetadata(); + if (metadata != null) { + title = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE); + if (TextUtils.isEmpty(title)) { + title = metadata.getString(MediaMetadata.METADATA_KEY_TITLE); + } + art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); + if (art == null) { + art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART); + } + } + } + if (!TextUtils.equals(title, mMediaTitle) || art != mArt) { + mMediaTitle = title; + mArt = art; + return true; + } + return false; + } + + + private String getNotificationTitle() { + if (!TextUtils.isEmpty(mMediaTitle)) { + return mMediaTitle; + } + + final String applicationTitle = getApplicationLabel(mPackageName); + if (!TextUtils.isEmpty(applicationTitle)) { + return applicationTitle; + } + + return mDefaultTitle; + } + + private String getApplicationLabel(String packageName) { + try { + final ApplicationInfo appInfo = mPackageManager.getApplicationInfo(packageName, 0); + return mPackageManager.getApplicationLabel(appInfo).toString(); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + + private static PendingIntent createPendingIntent(Context context, String action) { + return PendingIntent.getBroadcast(context, 0, + new Intent(action), PendingIntent.FLAG_CANCEL_CURRENT); + } +} 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..f3dadfcb933a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java @@ -0,0 +1,96 @@ +/* + * 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 { + // NOTE: Since we enable these from the same WM ShellCommand, these names should not conflict + // with those in the framework ProtoLogGroup + WM_SHELL_TASK_ORG(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, + Consts.TAG_WM_SHELL), + WM_SHELL_TRANSITIONS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, + 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..66ecf453c362 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java @@ -0,0 +1,135 @@ +/* + * 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; + + 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 int startTextLogging(Context context, String[] groups, PrintWriter pw) { + try { + mViewerConfig.loadViewerConfig( + context.getResources().openRawResource(R.raw.wm_shell_protolog)); + return setLogging(true /* setTextLogging */, true, pw, 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); + } + return -1; + } + + public int stopTextLogging(String[] groups, PrintWriter pw) { + return setLogging(true /* setTextLogging */, false, pw, groups); + } + + private ShellProtoLogImpl() { + super(new File(LOG_FILENAME), null, BUFFER_CAPACITY, new ProtoLogViewerConfigReader()); + } +} + 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/tests/src/com/android/wm/shell/tests/WindowManagerShellTest.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerState.java index 376875b143a1..23d86a00d4bf 100644 --- a/libs/WindowManager/Shell/tests/src/com/android/wm/shell/tests/WindowManagerShellTest.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerState.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * 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. @@ -14,27 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.tests; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.wm.shell.WindowManagerShell; - -import org.junit.Test; -import org.junit.runner.RunWith; +package com.android.wm.shell.splitscreen; /** - * Tests for the shell. + * Class to hold state of divider that needs to persist across configuration changes. */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class WindowManagerShellTest { - - WindowManagerShell mShell; - - @Test - public void testNothing() { - // Do nothing - } +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..2b14e8bf88d6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/DividerView.java @@ -0,0 +1,1339 @@ +/* + * 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) { + setHidden(false); + + 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() { + // The view is going to be removed right after this function involved, updates the surface + // in the current thread instead of posting it to the view's UI thread. + final SurfaceControl sc = getWindowSurfaceControl(); + if (sc == null) { + return; + } + Transaction t = mTiles.getTransaction(); + t.hide(sc); + mImeController.setDimsHidden(t, true); + t.apply(); + mTiles.releaseTransaction(t); + + // Reset tile bounds + 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 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..985dff20ad32 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java @@ -0,0 +1,86 @@ +/* + * 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); + + /** 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 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(); + + /** + * Splits the primary task if feasible, this is to preserve legacy way to toggle split screen. + * Like triggering split screen through long pressing recents app button or through + * {@link android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN}. + * + * @return {@code true} if it successes to split the primary task. + */ + boolean splitPrimaryTask(); +} 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..43e4d62baaf6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -0,0 +1,567 @@ +/* + * 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.ActivityManager.LOCK_TASK_MODE_PINNED; +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.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.view.Display.DEFAULT_DISPLAY; + +import android.app.ActivityManager.RunningTaskInfo; +import android.app.ActivityTaskManager; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.Handler; +import android.os.RemoteException; +import android.provider.Settings; +import android.util.Slog; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Toast; +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.SyncTransactionQueue; +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.List; +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 = "SplitScreenCtrl"; + 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, SyncTransactionQueue syncQueue) { + mContext = context; + mDisplayController = displayController; + mSystemWindows = systemWindows; + mImeController = imeController; + mHandler = handler; + mForcedResizableController = new ForcedResizableInfoActivityController(context, this); + mTransactionPool = transactionPool; + mWindowManagerProxy = new WindowManagerProxy(syncQueue, 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. + } + + @Override + public boolean isSplitScreenSupported() { + return mSplits.isSplitScreenSupported(); + } + + @Override + 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); + } + + @Override + public DividerView getDividerView() { + return mView; + } + + @Override + public boolean isMinimized() { + return mMinimized; + } + + @Override + public boolean isHomeStackResizable() { + return mHomeStackResizable; + } + + @Override + 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; + }); + } + } + } + + @Override + 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); + } + + @Override + public void onActivityForcedResizable(String packageName, int taskId, int reason) { + mForcedResizableController.activityForcedResizable(packageName, taskId, reason); + } + + @Override + public void onActivityDismissingSplitScreen() { + mForcedResizableController.activityDismissingSplitScreen(); + } + + @Override + public void onActivityLaunchOnSecondaryDisplayFailed() { + mForcedResizableController.activityLaunchOnSecondaryDisplayFailed(); + } + + @Override + public void onUndockingTask() { + if (mView != null) { + mView.onUndockingTask(); + } + } + + @Override + public void onAppTransitionFinished() { + if (mView == null) { + return; + } + mForcedResizableController.onAppTransitionFinished(); + } + + @Override + 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); + } + + @Override + 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)); + } + } + + @Override + public boolean splitPrimaryTask() { + try { + if (ActivityTaskManager.getService().getLockTaskModeState() == LOCK_TASK_MODE_PINNED + || isSplitActive()) { + return false; + } + + // Try fetching the top running task. + final List<RunningTaskInfo> runningTasks = + ActivityTaskManager.getService().getTasks(1 /* maxNum */); + if (runningTasks == null || runningTasks.isEmpty()) { + return false; + } + // Note: The set of running tasks from the system is ordered by recency. + final RunningTaskInfo topRunningTask = runningTasks.get(0); + + final int activityType = topRunningTask.configuration.windowConfiguration + .getActivityType(); + if (activityType == ACTIVITY_TYPE_HOME || activityType == ACTIVITY_TYPE_RECENTS) { + return false; + } + + if (!topRunningTask.supportsSplitScreenMultiWindow) { + Toast.makeText(mContext, R.string.dock_non_resizeble_failed_to_dock_text, + Toast.LENGTH_SHORT).show(); + return false; + } + + return ActivityTaskManager.getService().setTaskWindowingModeSplitScreenPrimary( + topRunningTask.taskId, true /* onTop */); + } catch (RemoteException e) { + return false; + } + } + + /** 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; + // Resets divider bar position to undefined, so new divider bar will apply default position + // next time entering split mode. + mDividerState.mRatioPositionBeforeMinimized = 0; + 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; + } + + @Override + 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..f763d6d714c4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTaskOrganizer.java @@ -0,0 +1,249 @@ +/* + * 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 static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_SPLIT_SCREEN; +import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString; + +import android.app.ActivityManager.RunningTaskInfo; +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 androidx.annotation.NonNull; + +import com.android.wm.shell.ShellTaskOrganizer; + +import java.io.PrintWriter; + +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.addListenerForType(this, TASK_LISTENER_TYPE_SPLIT_SCREEN); + } + + void init() throws RemoteException { + synchronized (this) { + try { + mPrimary = mTaskOrganizer.createRootTask(Display.DEFAULT_DISPLAY, + WINDOWING_MODE_SPLIT_SCREEN_PRIMARY); + mSecondary = mTaskOrganizer.createRootTask(Display.DEFAULT_DISPLAY, + 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(); + } + } + + @Override + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + final String childPrefix = innerPrefix + " "; + pw.println(prefix + this); + } + + @Override + public String toString() { + return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_SPLIT_SCREEN); + } +} 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..47e7c99d2268 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/WindowManagerProxy.java @@ -0,0 +1,366 @@ +/* + * 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.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 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(SyncTransactionQueue syncQueue, TaskOrganizer taskOrganizer) { + mSyncTransactionQueue = syncQueue; + 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 + // getAllRootTaskInfos 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/README.md b/libs/WindowManager/Shell/tests/README.md new file mode 100644 index 000000000000..c19db76a358c --- /dev/null +++ b/libs/WindowManager/Shell/tests/README.md @@ -0,0 +1,15 @@ +# WM Shell Test + +This contains all tests written for WM (WindowManager) Shell and it's currently +divided into 3 categories + +- unittest, tests against individual functions, usually @SmallTest and do not + require UI automation nor real device to run +- integration, this maybe a mix of functional and integration tests. Contains + tests verify the WM Shell as a whole, like talking to WM core. This usually + involves mocking the window manager service or even talking to the real one. + Due to this nature, test cases in this package is normally annotated as + @LargeTest and runs with UI automation on real device +- flicker, similar to functional tests with its sole focus on flickerness. See + [WM Shell Flicker Test Package](http://cs/android/framework/base/libs/WindowManager/Shell/tests/flicker/) + for more details diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp new file mode 100644 index 000000000000..d7afa0e166b3 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/Android.bp @@ -0,0 +1,52 @@ +// +// 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. +// + +android_test { + name: "WMShellFlickerTests", + srcs: ["src/**/*.java", "src/**/*.kt"], + manifest: "AndroidManifest.xml", + test_config: "AndroidTestPhysicalDevices.xml", + platform_apis: true, + certificate: "platform", + test_suites: ["device-tests"], + libs: ["android.test.runner"], + static_libs: [ + "androidx.test.ext.junit", + "flickerlib", + "truth-prebuilt", + "app-helpers-core", + "launcher-helper-lib", + "launcher-aosp-tapl" + ], +} + +android_test { + name: "WMShellFlickerTestsVirtual", + srcs: ["src/**/*.java", "src/**/*.kt"], + manifest: "AndroidManifest.xml", + test_config: "AndroidTestVirtualDevices.xml", + platform_apis: true, + certificate: "platform", + libs: ["android.test.runner"], + static_libs: [ + "androidx.test.ext.junit", + "flickerlib", + "truth-prebuilt", + "app-helpers-core", + "launcher-helper-lib", + "launcher-aosp-tapl" + ], +} diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml new file mode 100644 index 000000000000..8b2f6681554a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml @@ -0,0 +1,43 @@ +<?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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.wm.shell.flicker"> + + <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/> + <!-- Read and write traces from external storage --> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + <!-- Write secure settings --> + <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> + <!-- Capture screen contents --> + <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" /> + <!-- Enable / Disable tracing !--> + <uses-permission android:name="android.permission.DUMP" /> + <!-- Run layers trace --> + <uses-permission android:name="android.permission.HARDWARE_TEST"/> + <!-- Workaround grant runtime permission exception from b/152733071 --> + <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/> + <uses-permission android:name="android.permission.READ_LOGS"/> + <application> + <uses-library android:name="android.test.runner"/> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.wm.shell.flicker" + android:label="WindowManager Shell Flicker Tests"> + </instrumentation> +</manifest> diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTestPhysicalDevices.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTestPhysicalDevices.xml new file mode 100644 index 000000000000..9dd9f42bdf81 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/AndroidTestPhysicalDevices.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * Copyright 2020 Google Inc. All Rights Reserved. + --> +<configuration description="Runs WindowManager Shell Flicker Tests"> + <option name="test-tag" value="FlickerTests" /> + <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <!-- keeps the screen on during tests --> + <option name="screen-always-on" value="on" /> + <!-- prevents the phone from restarting --> + <option name="force-skip-system-props" value="true" /> + <!-- set WM tracing verbose level to all --> + <option name="run-command" value="cmd window tracing level all" /> + <!-- inform WM to log all transactions --> + <option name="run-command" value="cmd window tracing transaction" /> + <!-- restart launcher to activate TAPL --> + <option name="run-command" value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher" /> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.DeviceCleaner"> + <!-- reboot the device to teardown any crashed tests --> + <option name="cleanup-action" value="REBOOT" /> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true"/> + <option name="test-file-name" value="WMShellFlickerTests.apk"/> + <option name="test-file-name" value="WMShellFlickerTestApp.apk" /> + </target_preparer> + <test class="com.android.tradefed.testtype.AndroidJUnitTest"> + <option name="package" value="com.android.wm.shell.flicker"/> + <option name="include-annotation" value="androidx.test.filters.RequiresDevice" /> + <option name="exclude-annotation" value="androidx.test.filters.FlakyTest" /> + <option name="shell-timeout" value="6600s" /> + <option name="test-timeout" value="6000s" /> + <option name="hidden-api-checks" value="false" /> + </test> + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="directory-keys" value="/storage/emulated/0/Android/data/com.android.wm.shell.flicker/files" /> + <option name="collect-on-run-ended-only" value="true" /> + <option name="clean-up" value="true" /> + </metrics_collector> +</configuration>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTestVirtualDevices.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTestVirtualDevices.xml new file mode 100644 index 000000000000..afb1166415fc --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/AndroidTestVirtualDevices.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * Copyright 2020 Google Inc. All Rights Reserved. + --> +<configuration description="Runs WindowManager Shell Flicker Tests"> + <option name="test-tag" value="FlickerTests" /> + <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <!-- keeps the screen on during tests --> + <option name="screen-always-on" value="on" /> + <!-- prevents the phone from restarting --> + <option name="force-skip-system-props" value="true" /> + <!-- set WM tracing verbose level to all --> + <option name="run-command" value="cmd window tracing level all" /> + <!-- inform WM to log all transactions --> + <option name="run-command" value="cmd window tracing transaction" /> + <!-- restart launcher to activate TAPL --> + <option name="run-command" value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher" /> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.DeviceCleaner"> + <!-- reboot the device to teardown any crashed tests --> + <option name="cleanup-action" value="REBOOT" /> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true"/> + <option name="test-file-name" value="WMShellFlickerTests.apk"/> + <option name="test-file-name" value="WMShellFlickerTestApp.apk" /> + </target_preparer> + <test class="com.android.tradefed.testtype.AndroidJUnitTest"> + <option name="package" value="com.android.wm.shell.flicker"/> + <option name="exclude-annotation" value="androidx.test.filters.RequiresDevice" /> + <option name="exclude-annotation" value="androidx.test.filters.FlakyTest" /> + <option name="shell-timeout" value="6600s" /> + <option name="test-timeout" value="6000s" /> + <option name="hidden-api-checks" value="false" /> + </test> + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="directory-keys" value="/storage/emulated/0/Android/data/com.android.wm.shell.flicker/files" /> + <option name="collect-on-run-ended-only" value="true" /> + <option name="clean-up" value="true" /> + </metrics_collector> +</configuration>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/README.md b/libs/WindowManager/Shell/tests/flicker/README.md new file mode 100644 index 000000000000..4502d498a65b --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/README.md @@ -0,0 +1,10 @@ +# WM Shell Flicker Test Package + +Please reference the following links + +- [Introduction to Flicker Test Library](http://cs/android/platform_testing/libraries/flicker/) +- [Flicker Test in frameworks/base](http://cs/android/frameworks/base/tests/FlickerTests/) + +on what is Flicker Test and how to write a Flicker Test + +To run the Flicker Tests for WM Shell, simply run `atest WMShellFlickerTests` diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt new file mode 100644 index 000000000000..8c4f5468906f --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt @@ -0,0 +1,153 @@ +/* + * 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.flicker + +import com.android.server.wm.flicker.dsl.EventLogAssertion +import com.android.server.wm.flicker.dsl.LayersAssertion +import com.android.server.wm.flicker.dsl.WmAssertion +import com.android.server.wm.flicker.helpers.WindowUtils + +@JvmOverloads +fun WmAssertion.statusBarWindowIsAlwaysVisible( + bugId: Int = 0, + enabled: Boolean = bugId == 0 +) { + all("statusBarWindowIsAlwaysVisible", bugId, enabled) { + this.showsAboveAppWindow(FlickerTestBase.STATUS_BAR_WINDOW_TITLE) + } +} + +@JvmOverloads +fun WmAssertion.navBarWindowIsAlwaysVisible( + bugId: Int = 0, + enabled: Boolean = bugId == 0 +) { + all("navBarWindowIsAlwaysVisible", bugId, enabled) { + this.showsAboveAppWindow(FlickerTestBase.NAVIGATION_BAR_WINDOW_TITLE) + } +} + +@JvmOverloads +fun LayersAssertion.noUncoveredRegions( + beginRotation: Int, + endRotation: Int = beginRotation, + allStates: Boolean = true, + bugId: Int = 0, + enabled: Boolean = bugId == 0 +) { + val startingBounds = WindowUtils.getDisplayBounds(beginRotation) + val endingBounds = WindowUtils.getDisplayBounds(endRotation) + if (allStates) { + all("noUncoveredRegions", bugId, enabled) { + if (startingBounds == endingBounds) { + this.coversAtLeastRegion(startingBounds) + } else { + this.coversAtLeastRegion(startingBounds) + .then() + .coversAtLeastRegion(endingBounds) + } + } + } else { + start("noUncoveredRegions_StartingPos") { + this.coversAtLeastRegion(startingBounds) + } + end("noUncoveredRegions_EndingPos") { + this.coversAtLeastRegion(endingBounds) + } + } +} + +@JvmOverloads +fun LayersAssertion.navBarLayerIsAlwaysVisible( + bugId: Int = 0, + enabled: Boolean = bugId == 0 +) { + all("navBarLayerIsAlwaysVisible", bugId, enabled) { + this.showsLayer(FlickerTestBase.NAVIGATION_BAR_WINDOW_TITLE) + } +} + +@JvmOverloads +fun LayersAssertion.statusBarLayerIsAlwaysVisible( + bugId: Int = 0, + enabled: Boolean = bugId == 0 +) { + all("statusBarLayerIsAlwaysVisible", bugId, enabled) { + this.showsLayer(FlickerTestBase.STATUS_BAR_WINDOW_TITLE) + } +} + +@JvmOverloads +fun LayersAssertion.navBarLayerRotatesAndScales( + beginRotation: Int, + endRotation: Int = beginRotation, + bugId: Int = 0, + enabled: Boolean = bugId == 0 +) { + val startingPos = WindowUtils.getNavigationBarPosition(beginRotation) + val endingPos = WindowUtils.getNavigationBarPosition(endRotation) + + start("navBarLayerRotatesAndScales_StartingPos", bugId, enabled) { + this.hasVisibleRegion(FlickerTestBase.NAVIGATION_BAR_WINDOW_TITLE, startingPos) + } + end("navBarLayerRotatesAndScales_EndingPost", bugId, enabled) { + this.hasVisibleRegion(FlickerTestBase.NAVIGATION_BAR_WINDOW_TITLE, endingPos) + } + + if (startingPos == endingPos) { + all("navBarLayerRotatesAndScales", bugId, enabled) { + this.hasVisibleRegion(FlickerTestBase.NAVIGATION_BAR_WINDOW_TITLE, startingPos) + } + } +} + +@JvmOverloads +fun LayersAssertion.statusBarLayerRotatesScales( + beginRotation: Int, + endRotation: Int = beginRotation, + bugId: Int = 0, + enabled: Boolean = bugId == 0 +) { + val startingPos = WindowUtils.getStatusBarPosition(beginRotation) + val endingPos = WindowUtils.getStatusBarPosition(endRotation) + + start("statusBarLayerRotatesScales_StartingPos", bugId, enabled) { + this.hasVisibleRegion(FlickerTestBase.STATUS_BAR_WINDOW_TITLE, startingPos) + } + end("statusBarLayerRotatesScales_EndingPos", bugId, enabled) { + this.hasVisibleRegion(FlickerTestBase.STATUS_BAR_WINDOW_TITLE, endingPos) + } +} + +fun EventLogAssertion.focusChanges( + vararg windows: String, + bugId: Int = 0, + enabled: Boolean = bugId == 0 +) { + all(enabled = enabled, bugId = bugId) { + this.focusChanges(windows) + } +} + +fun EventLogAssertion.focusDoesNotChange( + bugId: Int = 0, + enabled: Boolean = bugId == 0 +) { + all(enabled = enabled, bugId = bugId) { + this.focusDoesNotChange() + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt new file mode 100644 index 000000000000..99f824bb8341 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt @@ -0,0 +1,130 @@ +/* + * 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.flicker + +import android.os.RemoteException +import android.os.SystemClock +import android.platform.helpers.IAppHelper +import android.view.Surface +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.server.wm.flicker.Flicker + +/** + * Base class of all Flicker test that performs common functions for all flicker tests: + * + * + * - Caches transitions so that a transition is run once and the transition results are used by + * tests multiple times. This is needed for parameterized tests which call the BeforeClass methods + * multiple times. + * - Keeps track of all test artifacts and deletes ones which do not need to be reviewed. + * - Fails tests if results are not available for any test due to jank. + */ +abstract class FlickerTestBase { + val instrumentation by lazy { + InstrumentationRegistry.getInstrumentation() + } + val uiDevice by lazy { + UiDevice.getInstance(instrumentation) + } + + /** + * Build a test tag for the test + * @param testName Name of the transition(s) being tested + * @param app App being launcher + * @param rotation Initial screen rotation + * + * @return test tag with pattern <NAME>__<APP>__<ROTATION> + </ROTATION></APP></NAME> */ + protected fun buildTestTag(testName: String, app: IAppHelper, rotation: Int): String { + return buildTestTag( + testName, app, rotation, rotation, app2 = null, extraInfo = "") + } + + /** + * Build a test tag for the test + * @param testName Name of the transition(s) being tested + * @param app App being launcher + * @param beginRotation Initial screen rotation + * @param endRotation End screen rotation (if any, otherwise use same as initial) + * + * @return test tag with pattern <NAME>__<APP>__<BEGIN_ROTATION>-<END_ROTATION> + </END_ROTATION></BEGIN_ROTATION></APP></NAME> */ + protected fun buildTestTag( + testName: String, + app: IAppHelper, + beginRotation: Int, + endRotation: Int + ): String { + return buildTestTag( + testName, app, beginRotation, endRotation, app2 = null, extraInfo = "") + } + + /** + * Build a test tag for the test + * @param testName Name of the transition(s) being tested + * @param app App being launcher + * @param app2 Second app being launched (if any) + * @param beginRotation Initial screen rotation + * @param endRotation End screen rotation (if any, otherwise use same as initial) + * @param extraInfo Additional information to append to the tag + * + * @return test tag with pattern <NAME>__<APP></APP>(S)>__<ROTATION></ROTATION>(S)>[__<EXTRA>] + </EXTRA></NAME> */ + protected fun buildTestTag( + testName: String, + app: IAppHelper, + beginRotation: Int, + endRotation: Int, + app2: IAppHelper?, + extraInfo: String + ): String { + var testTag = "${testName}__${app.launcherName}" + if (app2 != null) { + testTag += "-${app2.launcherName}" + } + testTag += "__${Surface.rotationToString(beginRotation)}" + if (endRotation != beginRotation) { + testTag += "-${Surface.rotationToString(endRotation)}" + } + if (extraInfo.isNotEmpty()) { + testTag += "__$extraInfo" + } + return testTag + } + + protected fun Flicker.setRotation(rotation: Int) { + try { + when (rotation) { + Surface.ROTATION_270 -> device.setOrientationLeft() + Surface.ROTATION_90 -> device.setOrientationRight() + Surface.ROTATION_0 -> device.setOrientationNatural() + else -> device.setOrientationNatural() + } + // Wait for animation to complete + SystemClock.sleep(1000) + } catch (e: RemoteException) { + throw RuntimeException(e) + } + } + + companion object { + const val NAVIGATION_BAR_WINDOW_TITLE = "NavigationBar" + const val STATUS_BAR_WINDOW_TITLE = "StatusBar" + const val DOCKED_STACK_DIVIDER = "DockedStackDivider" + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NonRotationTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NonRotationTestBase.kt new file mode 100644 index 000000000000..90334ae91e9d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NonRotationTestBase.kt @@ -0,0 +1,36 @@ +/* + * 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.flicker + +import android.view.Surface +import org.junit.runners.Parameterized + +abstract class NonRotationTestBase( + protected val rotationName: String, + protected val rotation: Int +) : FlickerTestBase() { + companion object { + const val SCREENSHOT_LAYER = "RotationLayer" + + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val supportedRotations = intArrayOf(Surface.ROTATION_0, Surface.ROTATION_90) + return supportedRotations.map { arrayOf(Surface.rotationToString(it), it) } + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FlickerAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FlickerAppHelper.kt new file mode 100644 index 000000000000..47a62ce92d11 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FlickerAppHelper.kt @@ -0,0 +1,31 @@ +/* + * 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.flicker.helpers + +import android.app.Instrumentation +import android.support.test.launcherhelper.ILauncherStrategy +import com.android.server.wm.flicker.helpers.StandardAppHelper + +abstract class FlickerAppHelper( + instr: Instrumentation, + launcherName: String, + launcherStrategy: ILauncherStrategy +) : StandardAppHelper(instr, sFlickerPackage, launcherName, launcherStrategy) { + companion object { + var sFlickerPackage = "com.android.wm.shell.flicker.testapp" + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt new file mode 100644 index 000000000000..0cedc0a7147f --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt @@ -0,0 +1,53 @@ +/* + * 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.flicker.helpers + +import android.app.Instrumentation +import android.support.test.launcherhelper.ILauncherStrategy +import android.support.test.launcherhelper.LauncherStrategyFactory +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.helpers.FIND_TIMEOUT +import com.android.server.wm.flicker.helpers.waitForIME +import org.junit.Assert + +open class ImeAppHelper( + instr: Instrumentation, + launcherName: String = "ImeApp", + launcherStrategy: ILauncherStrategy = LauncherStrategyFactory + .getInstance(instr) + .launcherStrategy +) : FlickerAppHelper(instr, launcherName, launcherStrategy) { + open fun openIME(device: UiDevice) { + val editText = device.wait( + Until.findObject(By.res(getPackage(), "plain_text_input")), + FIND_TIMEOUT) + Assert.assertNotNull("Text field not found, this usually happens when the device " + + "was left in an unknown state (e.g. in split screen)", editText) + editText.click() + if (!device.waitForIME()) { + Assert.fail("IME did not appear") + } + } + + open fun closeIME(device: UiDevice) { + device.pressBack() + // Using only the AccessibilityInfo it is not possible to identify if the IME is active + device.waitForIdle(1000) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt new file mode 100644 index 000000000000..539170202b8a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt @@ -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.flicker.helpers + +import android.app.Instrumentation +import android.support.test.launcherhelper.ILauncherStrategy +import android.support.test.launcherhelper.LauncherStrategyFactory +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiDevice +import com.android.server.wm.flicker.helpers.hasPipWindow +import com.android.server.wm.flicker.helpers.closePipWindow +import org.junit.Assert + +class PipAppHelper( + instr: Instrumentation, + launcherStrategy: ILauncherStrategy = LauncherStrategyFactory + .getInstance(instr) + .launcherStrategy +) : FlickerAppHelper(instr, "PipApp", launcherStrategy) { + fun clickEnterPipButton(device: UiDevice) { + val enterPipButton = device.findObject(By.res(getPackage(), "enter_pip")) + Assert.assertNotNull("Pip button not found, this usually happens when the device " + + "was left in an unknown state (e.g. in split screen)", enterPipButton) + enterPipButton.click() + device.hasPipWindow() + } + + fun closePipWindow(device: UiDevice) { + device.closePipWindow() + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt new file mode 100644 index 000000000000..010aa0d7d832 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt @@ -0,0 +1,121 @@ +/* + * 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.flicker.pip + +import android.view.Surface +import androidx.test.filters.FlakyTest +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.dsl.flicker +import com.android.server.wm.flicker.helpers.closePipWindow +import com.android.server.wm.flicker.helpers.expandPipWindow +import com.android.server.wm.flicker.helpers.hasPipWindow +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.wm.shell.flicker.navBarLayerIsAlwaysVisible +import com.android.wm.shell.flicker.navBarLayerRotatesAndScales +import com.android.wm.shell.flicker.navBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.noUncoveredRegions +import com.android.wm.shell.flicker.statusBarLayerIsAlwaysVisible +import com.android.wm.shell.flicker.statusBarLayerRotatesScales +import com.android.wm.shell.flicker.statusBarWindowIsAlwaysVisible +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test Pip launch. + * To run this test: `atest WMShellFlickerTests:PipToAppTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@FlakyTest(bugId = 152738416) +class EnterPipTest( + rotationName: String, + rotation: Int +) : PipTestBase(rotationName, rotation) { + @Test + fun test() { + flicker(instrumentation) { + withTag { buildTestTag("enterPip", testApp, rotation) } + repeat { 1 } + setup { + test { + device.wakeUpAndGoToHomeScreen() + } + eachRun { + device.pressHome() + testApp.open() + this.setRotation(rotation) + } + } + teardown { + eachRun { + if (device.hasPipWindow()) { + device.closePipWindow() + } + testApp.exit() + this.setRotation(Surface.ROTATION_0) + } + test { + if (device.hasPipWindow()) { + device.closePipWindow() + } + } + } + transitions { + testApp.clickEnterPipButton(device) + device.expandPipWindow() + } + assertions { + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + all("pipWindowBecomesVisible") { + this.showsAppWindow(testApp.`package`) + .then() + .showsAppWindow(sPipWindowTitle) + } + } + + layersTrace { + navBarLayerIsAlwaysVisible() + statusBarLayerIsAlwaysVisible() + noUncoveredRegions(rotation, Surface.ROTATION_0, allStates = false) + navBarLayerRotatesAndScales(rotation, Surface.ROTATION_0) + statusBarLayerRotatesScales(rotation, Surface.ROTATION_0) + + all("pipLayerBecomesVisible") { + this.showsLayer(testApp.launcherName) + .then() + .showsLayer(sPipWindowTitle) + } + } + } + } + } + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val supportedRotations = intArrayOf(Surface.ROTATION_0) + return supportedRotations.map { arrayOf(Surface.rotationToString(it), it) } + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt new file mode 100644 index 000000000000..43e022538685 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt @@ -0,0 +1,217 @@ +/* + * 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.flicker.pip + +import android.content.ComponentName +import android.graphics.Region +import android.support.test.launcherhelper.LauncherStrategyFactory +import android.util.Log +import android.view.Surface +import android.view.WindowManager +import androidx.test.filters.RequiresDevice +import com.android.compatibility.common.util.SystemUtil +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.dsl.runWithFlicker +import com.android.server.wm.flicker.helpers.closePipWindow +import com.android.server.wm.flicker.helpers.hasPipWindow +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.wm.shell.flicker.helpers.ImeAppHelper +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized +import java.io.IOException + +/** + * Test Pip launch. + * To run this test: `atest WMShellFlickerTests:PipKeyboardTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class PipKeyboardTest( + rotationName: String, + rotation: Int +) : PipTestBase(rotationName, rotation) { + private val windowManager: WindowManager = + instrumentation.context.getSystemService(WindowManager::class.java) + + private val keyboardApp = ImeAppHelper(instrumentation, "ImeApp", + LauncherStrategyFactory.getInstance(instrumentation).launcherStrategy) + + private val KEYBOARD_ACTIVITY: ComponentName = ComponentName.createRelative( + "com.android.wm.shell.flicker.testapp", ".ImeActivity") + private val PIP_ACTIVITY_WINDOW_NAME = "PipActivity" + private val INPUT_METHOD_WINDOW_NAME = "InputMethod" + + private val testRepetitions = 10 + + private val keyboardScenario: FlickerBuilder + get() = FlickerBuilder(instrumentation).apply { + repeat { testRepetitions } + // disable layer tracing + withLayerTracing { null } + setup { + test { + device.wakeUpAndGoToHomeScreen() + device.pressHome() + // launch our target pip app + testApp.open() + this.setRotation(rotation) + testApp.clickEnterPipButton(device) + // open an app with an input field and a keyboard + // UiAutomator doesn't support to launch the multiple Activities in a task. + // So use launchActivity() for the Keyboard Activity. + launchActivity(KEYBOARD_ACTIVITY) + } + } + teardown { + test { + keyboardApp.exit() + + if (device.hasPipWindow()) { + device.closePipWindow() + } + testApp.exit() + this.setRotation(Surface.ROTATION_0) + } + } + } + + /** Ensure the pip window remains visible throughout any keyboard interactions. */ + @Test + fun pipWindow_doesNotLeaveTheScreen_onKeyboardOpenClose() { + val testTag = "pipWindow_doesNotLeaveTheScreen_onKeyboardOpenClose" + runWithFlicker(keyboardScenario) { + withTestName { testTag } + transitions { + // open the soft keyboard + keyboardApp.openIME(device) + + // then close it again + keyboardApp.closeIME(device) + } + assertions { + windowManagerTrace { + all("PiP window must remain inside visible bounds") { + coversAtMostRegion( + partialWindowTitle = "PipActivity", + region = Region(windowManager.maximumWindowMetrics.bounds) + ) + } + } + } + } + } + + /** Ensure the pip window does not obscure the keyboard. */ + @Test + fun pipWindow_doesNotObscure_keyboard() { + val testTag = "pipWindow_doesNotObscure_keyboard" + runWithFlicker(keyboardScenario) { + withTestName { testTag } + transitions { + // open the soft keyboard + keyboardApp.openIME(device) + } + teardown { + eachRun { + // close the keyboard + keyboardApp.closeIME(device) + } + } + assertions { + windowManagerTrace { + end { + isAboveWindow(INPUT_METHOD_WINDOW_NAME, PIP_ACTIVITY_WINDOW_NAME) + } + } + } + } + } + + private fun launchActivity( + activity: ComponentName? = null, + action: String? = null, + flags: Set<Int> = setOf(), + boolExtras: Map<String, Boolean> = mapOf(), + intExtras: Map<String, Int> = mapOf(), + stringExtras: Map<String, String> = mapOf() + ) { + require(activity != null || !action.isNullOrBlank()) { + "Cannot launch an activity with neither activity name nor action!" + } + val command = composeCommand( + "start", activity, action, flags, boolExtras, intExtras, stringExtras) + executeShellCommand(command) + } + + private fun composeCommand( + command: String, + activity: ComponentName?, + action: String?, + flags: Set<Int>, + boolExtras: Map<String, Boolean>, + intExtras: Map<String, Int>, + stringExtras: Map<String, String> + ): String = buildString { + append("am ") + append(command) + activity?.let { + append(" -n ") + append(it.flattenToShortString()) + } + action?.let { + append(" -a ") + append(it) + } + flags.forEach { + append(" -f ") + append(it) + } + boolExtras.forEach { + append(it.withFlag("ez")) + } + intExtras.forEach { + append(it.withFlag("ei")) + } + stringExtras.forEach { + append(it.withFlag("es")) + } + } + + private fun Map.Entry<String, *>.withFlag(flag: String): String = " --$flag $key $value" + + private fun executeShellCommand(cmd: String): String { + try { + return SystemUtil.runShellCommand(instrumentation, cmd) + } catch (e: IOException) { + Log.e("FlickerTests", "Error running shell command: $cmd") + throw e + } + } + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val supportedRotations = intArrayOf(Surface.ROTATION_0) + return supportedRotations.map { arrayOf(Surface.rotationToString(it), it) } + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTestBase.kt new file mode 100644 index 000000000000..3822d69a65f5 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTestBase.kt @@ -0,0 +1,31 @@ +/* + * 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.flicker.pip + +import com.android.wm.shell.flicker.NonRotationTestBase +import com.android.wm.shell.flicker.helpers.PipAppHelper + +abstract class PipTestBase( + rotationName: String, + rotation: Int +) : NonRotationTestBase(rotationName, rotation) { + protected val testApp = PipAppHelper(instrumentation) + + companion object { + const val sPipWindowTitle = "PipMenuActivity" + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/Android.bp b/libs/WindowManager/Shell/tests/flicker/test-apps/Android.bp new file mode 100644 index 000000000000..e69de29bb2d1 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/Android.bp diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp new file mode 100644 index 000000000000..d12b49245277 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp @@ -0,0 +1,20 @@ +// 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. + +android_test { + name: "WMShellFlickerTestApp", + srcs: ["**/*.java"], + sdk_version: "current", + test_suites: ["device-tests"], +} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml new file mode 100644 index 000000000000..7f8321f3fa3d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml @@ -0,0 +1,54 @@ +<?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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.wm.shell.flicker.testapp"> + + <uses-sdk android:minSdkVersion="29" + android:targetSdkVersion="29"/> + <application android:allowBackup="false" + android:supportsRtl="true"> + <activity android:name=".PipActivity" + android:resizeableActivity="true" + android:supportsPictureInPicture="true" + android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" + android:taskAffinity="com.android.wm.shell.flicker.testapp.PipActivity" + android:label="PipApp" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LEANBACK_LAUNCHER"/> + </intent-filter> + </activity> + <activity android:name=".ImeActivity" + android:taskAffinity="com.android.wm.shell.flicker.testapp.ImeActivity" + android:label="ImeApp" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LEANBACK_LAUNCHER"/> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_ime.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_ime.xml new file mode 100644 index 000000000000..4708cfd48381 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_ime.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2018 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. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:focusableInTouchMode="true" + android:background="@android:color/holo_green_light"> + <EditText android:id="@+id/plain_text_input" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:inputType="text"/> +</LinearLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml new file mode 100644 index 000000000000..e1870d9c523d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml @@ -0,0 +1,26 @@ +<?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. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@android:color/holo_blue_bright"> + <Button android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/enter_pip" + android:text="Enter PIP"/> +</LinearLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/ImeActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/ImeActivity.java new file mode 100644 index 000000000000..856728715c1c --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/ImeActivity.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 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.flicker.testapp; + +import android.app.Activity; +import android.os.Bundle; +import android.view.WindowManager; + +public class ImeActivity extends Activity { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + WindowManager.LayoutParams p = getWindow().getAttributes(); + p.layoutInDisplayCutoutMode = WindowManager.LayoutParams + .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + getWindow().setAttributes(p); + setContentView(R.layout.activity_ime); + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java new file mode 100644 index 000000000000..305281691e11 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.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.flicker.testapp; + +import android.app.Activity; +import android.app.PictureInPictureParams; +import android.graphics.Rect; +import android.os.Bundle; +import android.util.Rational; +import android.view.WindowManager; +import android.widget.Button; + +public class PipActivity extends Activity { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + WindowManager.LayoutParams p = getWindow().getAttributes(); + p.layoutInDisplayCutoutMode = WindowManager.LayoutParams + .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + getWindow().setAttributes(p); + setContentView(R.layout.activity_pip); + Button enterPip = (Button) findViewById(R.id.enter_pip); + + PictureInPictureParams params = new PictureInPictureParams.Builder() + .setAspectRatio(new Rational(1, 1)) + .setSourceRectHint(new Rect(0, 0, 100, 100)) + .build(); + + enterPip.setOnClickListener((v) -> enterPictureInPictureMode(params)); + } +} diff --git a/libs/WindowManager/Shell/tests/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp index 78fa45ebdf94..9940ea575873 100644 --- a/libs/WindowManager/Shell/tests/Android.bp +++ b/libs/WindowManager/Shell/tests/unittest/Android.bp @@ -13,7 +13,7 @@ // limitations under the License. android_test { - name: "WindowManagerShellTests", + name: "WMShellUnitTests", srcs: ["**/*.java"], @@ -23,21 +23,29 @@ android_test { "androidx.test.runner", "androidx.test.rules", "androidx.test.ext.junit", + "androidx.dynamicanimation_dynamicanimation", + "dagger2", + "kotlinx-coroutines-android", + "kotlinx-coroutines-core", "mockito-target-extended-minus-junit4", "truth-prebuilt", + "testables", ], + libs: [ "android.test.mock", "android.test.base", "android.test.runner", ], + jni_libs: [ "libdexmakerjvmtiagent", "libstaticjvmtiagent", ], - sdk_version: "current", - platform_apis: true, + kotlincflags: ["-Xjvm-default=enable"], + + plugins: ["dagger2-compiler"], optimize: { enabled: false, diff --git a/libs/WindowManager/Shell/tests/AndroidManifest.xml b/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml index a8f795ec8a8d..a8f795ec8a8d 100644 --- a/libs/WindowManager/Shell/tests/AndroidManifest.xml +++ b/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml diff --git a/libs/WindowManager/Shell/tests/AndroidTest.xml b/libs/WindowManager/Shell/tests/unittest/AndroidTest.xml index 4dce4db360e4..21ed2c075dff 100644 --- a/libs/WindowManager/Shell/tests/AndroidTest.xml +++ b/libs/WindowManager/Shell/tests/unittest/AndroidTest.xml @@ -17,12 +17,12 @@ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> <option name="cleanup-apks" value="true" /> <option name="install-arg" value="-t" /> - <option name="test-file-name" value="WindowManagerShellTests.apk" /> + <option name="test-file-name" value="WMShellUnitTests.apk" /> </target_preparer> <option name="test-suite-tag" value="apct" /> <option name="test-suite-tag" value="framework-base-presubmit" /> - <option name="test-tag" value="WindowManagerShellTests" /> + <option name="test-tag" value="WMShellUnitTests" /> <test class="com.android.tradefed.testtype.AndroidJUnitTest" > <option name="package" value="com.android.wm.shell.tests" /> <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> diff --git a/libs/WindowManager/Shell/tests/res/values/config.xml b/libs/WindowManager/Shell/tests/unittest/res/values/config.xml index c894eb0133b5..c894eb0133b5 100644 --- a/libs/WindowManager/Shell/tests/res/values/config.xml +++ b/libs/WindowManager/Shell/tests/unittest/res/values/config.xml 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 new file mode 100644 index 000000000000..07a6bda239c7 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java @@ -0,0 +1,251 @@ +/* + * 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; + +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; + +import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_MULTI_WINDOW; +import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_PIP; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager.RunningTaskInfo; +import android.content.pm.ParceledListSlice; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteException; +import android.view.SurfaceControl; +import android.window.ITaskOrganizer; +import android.window.ITaskOrganizerController; +import android.window.TaskAppearedInfo; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.TransactionPool; + +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; + +/** + * Tests for the shell task organizer. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ShellTaskOrganizerTests { + + @Mock + private ITaskOrganizerController mTaskOrganizerController; + + ShellTaskOrganizer mOrganizer; + private final SyncTransactionQueue mSyncTransactionQueue = mock(SyncTransactionQueue.class); + private final TransactionPool mTransactionPool = mock(TransactionPool.class); + private final ShellExecutor mTestExecutor = mock(ShellExecutor.class); + + private class TrackingTaskListener implements ShellTaskOrganizer.TaskListener { + final ArrayList<RunningTaskInfo> appeared = new ArrayList<>(); + final ArrayList<RunningTaskInfo> vanished = new ArrayList<>(); + final ArrayList<RunningTaskInfo> infoChanged = new ArrayList<>(); + + @Override + public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { + appeared.add(taskInfo); + } + + @Override + public void onTaskInfoChanged(RunningTaskInfo taskInfo) { + infoChanged.add(taskInfo); + } + + @Override + public void onTaskVanished(RunningTaskInfo taskInfo) { + vanished.add(taskInfo); + } + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + try { + doReturn(ParceledListSlice.<TaskAppearedInfo>emptyList()) + .when(mTaskOrganizerController).registerTaskOrganizer(any()); + } catch (RemoteException e) {} + mOrganizer = spy(new ShellTaskOrganizer(mTaskOrganizerController, mSyncTransactionQueue, + mTransactionPool, mTestExecutor, mTestExecutor)); + } + + @Test + public void registerOrganizer_sendRegisterTaskOrganizer() throws RemoteException { + mOrganizer.registerOrganizer(); + + verify(mTaskOrganizerController).registerTaskOrganizer(any(ITaskOrganizer.class)); + } + + @Test + public void testOneListenerPerType() { + mOrganizer.addListenerForType(new TrackingTaskListener(), TASK_LISTENER_TYPE_MULTI_WINDOW); + try { + mOrganizer.addListenerForType( + new TrackingTaskListener(), TASK_LISTENER_TYPE_MULTI_WINDOW); + fail("Expected exception due to already registered listener"); + } catch (Exception e) { + // Expected failure + } + } + + @Test + public void testRegisterWithExistingTasks() throws RemoteException { + // Setup some tasks + RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW); + ArrayList<TaskAppearedInfo> taskInfos = new ArrayList<>(); + taskInfos.add(new TaskAppearedInfo(task1, new SurfaceControl())); + taskInfos.add(new TaskAppearedInfo(task2, new SurfaceControl())); + doReturn(new ParceledListSlice(taskInfos)) + .when(mTaskOrganizerController).registerTaskOrganizer(any()); + + // Register and expect the tasks to be stored + mOrganizer.registerOrganizer(); + + // Check that the tasks are next reported when the listener is added + TrackingTaskListener listener = new TrackingTaskListener(); + mOrganizer.addListenerForType(listener, TASK_LISTENER_TYPE_MULTI_WINDOW); + assertTrue(listener.appeared.contains(task1)); + assertTrue(listener.appeared.contains(task2)); + } + + @Test + public void testAppearedVanished() { + RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + TrackingTaskListener listener = new TrackingTaskListener(); + mOrganizer.addListenerForType(listener, TASK_LISTENER_TYPE_MULTI_WINDOW); + mOrganizer.onTaskAppeared(taskInfo, null); + assertTrue(listener.appeared.contains(taskInfo)); + + mOrganizer.onTaskVanished(taskInfo); + assertTrue(listener.vanished.contains(taskInfo)); + } + + @Test + public void testAddListenerExistingTasks() { + RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + mOrganizer.onTaskAppeared(taskInfo, null); + + TrackingTaskListener listener = new TrackingTaskListener(); + mOrganizer.addListenerForType(listener, TASK_LISTENER_TYPE_MULTI_WINDOW); + assertTrue(listener.appeared.contains(taskInfo)); + } + + @Test + public void testWindowingModeChange() { + RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + TrackingTaskListener mwListener = new TrackingTaskListener(); + TrackingTaskListener pipListener = new TrackingTaskListener(); + mOrganizer.addListenerForType(mwListener, TASK_LISTENER_TYPE_MULTI_WINDOW); + mOrganizer.addListenerForType(pipListener, TASK_LISTENER_TYPE_PIP); + mOrganizer.onTaskAppeared(taskInfo, null); + assertTrue(mwListener.appeared.contains(taskInfo)); + assertTrue(pipListener.appeared.isEmpty()); + + taskInfo = createTaskInfo(1, WINDOWING_MODE_PINNED); + mOrganizer.onTaskInfoChanged(taskInfo); + assertTrue(mwListener.vanished.contains(taskInfo)); + assertTrue(pipListener.appeared.contains(taskInfo)); + } + + @Test + public void testAddListenerForTaskId_afterTypeListener() { + RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + TrackingTaskListener mwListener = new TrackingTaskListener(); + TrackingTaskListener task1Listener = new TrackingTaskListener(); + mOrganizer.addListenerForType(mwListener, TASK_LISTENER_TYPE_MULTI_WINDOW); + mOrganizer.onTaskAppeared(task1, null); + assertTrue(mwListener.appeared.contains(task1)); + + // Add task 1 specific listener + mOrganizer.addListenerForTaskId(task1Listener, 1); + assertTrue(mwListener.vanished.contains(task1)); + assertTrue(task1Listener.appeared.contains(task1)); + } + + @Test + public void testAddListenerForTaskId_beforeTypeListener() { + RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + TrackingTaskListener mwListener = new TrackingTaskListener(); + TrackingTaskListener task1Listener = new TrackingTaskListener(); + mOrganizer.onTaskAppeared(task1, null); + mOrganizer.addListenerForTaskId(task1Listener, 1); + assertTrue(task1Listener.appeared.contains(task1)); + + mOrganizer.addListenerForType(mwListener, TASK_LISTENER_TYPE_MULTI_WINDOW); + assertFalse(mwListener.appeared.contains(task1)); + } + + @Test + public void testGetTaskListener() { + RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + + TrackingTaskListener mwListener = new TrackingTaskListener(); + mOrganizer.addListenerForType(mwListener, TASK_LISTENER_TYPE_MULTI_WINDOW); + + TrackingTaskListener cookieListener = new TrackingTaskListener(); + IBinder cookie = new Binder(); + task1.addLaunchCookie(cookie); + mOrganizer.setPendingLaunchCookieListener(cookie, cookieListener); + + // Priority goes to the cookie listener so we would expect the task appear to show up there + // instead of the multi-window type listener. + mOrganizer.onTaskAppeared(task1, null); + assertTrue(cookieListener.appeared.contains(task1)); + assertFalse(mwListener.appeared.contains(task1)); + + TrackingTaskListener task1Listener = new TrackingTaskListener(); + + boolean gotException = false; + try { + mOrganizer.addListenerForTaskId(task1Listener, 1); + } catch (Exception e) { + gotException = true; + } + // It should not be possible to add a task id listener for a task already mapped to a + // listener through cookie. + assertTrue(gotException); + } + + private RunningTaskInfo createTaskInfo(int taskId, int windowingMode) { + RunningTaskInfo taskInfo = new RunningTaskInfo(); + taskInfo.taskId = taskId; + taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); + return taskInfo; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/animation/PhysicsAnimatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/animation/PhysicsAnimatorTest.kt new file mode 100644 index 000000000000..4bd9bed26a82 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/animation/PhysicsAnimatorTest.kt @@ -0,0 +1,627 @@ +/* + * 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.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.util.ArrayMap +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.FloatPropertyCompat +import androidx.dynamicanimation.animation.SpringForce +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.wm.shell.animation.PhysicsAnimator.EndListener +import com.android.wm.shell.animation.PhysicsAnimator.UpdateListener +import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.clearAnimationUpdateFrames +import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.getAnimationUpdateFrames +import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.verifyAnimationUpdateFrames +import org.junit.After +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyFloat +import org.mockito.Mockito +import org.mockito.Mockito.mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.MockitoAnnotations + +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner::class) +@SmallTest +@Ignore("Blocking presubmits - investigating in b/158697054") +class PhysicsAnimatorTest : SysuiTestCase() { + private lateinit var viewGroup: ViewGroup + private lateinit var testView: View + private lateinit var testView2: View + + private lateinit var animator: PhysicsAnimator<View> + + private val springConfig = PhysicsAnimator.SpringConfig( + SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_NO_BOUNCY) + private val flingConfig = PhysicsAnimator.FlingConfig(2f) + + private lateinit var mockUpdateListener: UpdateListener<View> + private lateinit var mockEndListener: EndListener<View> + private lateinit var mockEndAction: Runnable + + private fun <T> eq(value: T): T = Mockito.eq(value) ?: value + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + mockUpdateListener = mock(UpdateListener::class.java) as UpdateListener<View> + mockEndListener = mock(EndListener::class.java) as EndListener<View> + mockEndAction = mock(Runnable::class.java) + + viewGroup = FrameLayout(context) + testView = View(context) + testView2 = View(context) + viewGroup.addView(testView) + viewGroup.addView(testView2) + + PhysicsAnimatorTestUtils.prepareForTest() + + // Most of our tests involve checking the end state of animations, so we want calls that + // start animations to block the test thread until the animations have ended. + PhysicsAnimatorTestUtils.setAllAnimationsBlock(true) + + animator = PhysicsAnimator.getInstance(testView) + } + + @After + fun tearDown() { + PhysicsAnimatorTestUtils.tearDown() + } + + @Test + fun testOneAnimatorPerView() { + assertEquals(animator, PhysicsAnimator.getInstance(testView)) + assertEquals(PhysicsAnimator.getInstance(testView), PhysicsAnimator.getInstance(testView)) + assertNotEquals(animator, PhysicsAnimator.getInstance(testView2)) + } + + @Test + fun testSpringOneProperty() { + animator + .spring(DynamicAnimation.TRANSLATION_X, 50f, springConfig) + .start() + + assertEquals(testView.translationX, 50f, 1f) + } + + @Test + fun testSpringMultipleProperties() { + animator + .spring(DynamicAnimation.TRANSLATION_X, 10f, springConfig) + .spring(DynamicAnimation.TRANSLATION_Y, 50f, springConfig) + .spring(DynamicAnimation.SCALE_Y, 1.1f, springConfig) + .start() + + assertEquals(10f, testView.translationX, 1f) + assertEquals(50f, testView.translationY, 1f) + assertEquals(1.1f, testView.scaleY, 0.01f) + } + + @Test + fun testFling() { + val startTime = System.currentTimeMillis() + + animator + .fling(DynamicAnimation.TRANSLATION_X, 1000f /* startVelocity */, flingConfig) + .fling(DynamicAnimation.TRANSLATION_Y, 500f, flingConfig) + .start() + + val elapsedTimeSeconds = (System.currentTimeMillis() - startTime) / 1000f + + // If the fling worked, the view should be somewhere between its starting position and the + // and the theoretical no-friction maximum of startVelocity (in pixels per second) + // multiplied by elapsedTimeSeconds. We can't calculate an exact expected location for a + // fling, so this is close enough. + assertTrue(testView.translationX > 0f) + assertTrue(testView.translationX < 1000f * elapsedTimeSeconds) + assertTrue(testView.translationY > 0f) + assertTrue(testView.translationY < 500f * elapsedTimeSeconds) + } + + @Test + @Throws(InterruptedException::class) + @Ignore("Increasingly flaky") + fun testEndListenersAndActions() { + PhysicsAnimatorTestUtils.setAllAnimationsBlock(false) + animator + .spring(DynamicAnimation.TRANSLATION_X, 10f, springConfig) + .spring(DynamicAnimation.TRANSLATION_Y, 500f, springConfig) + .addEndListener(mockEndListener) + .withEndActions(mockEndAction::run) + .start() + + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_X) + + // Once TRANSLATION_X is done, the view should be at x = 10... + assertEquals(10f, testView.translationX, 1f) + + // / ...TRANSLATION_Y should still be running... + assertTrue(animator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Y)) + + // ...and our end listener should have been called with x = 10, velocity = 0, and allEnded = + // false since TRANSLATION_Y is still running. + verify(mockEndListener).onAnimationEnd( + testView, + DynamicAnimation.TRANSLATION_X, + wasFling = false, + canceled = false, + finalValue = 10f, + finalVelocity = 0f, + allRelevantPropertyAnimsEnded = false) + verifyNoMoreInteractions(mockEndListener) + + // The end action should not have been run yet. + verify(mockEndAction, times(0)).run() + + // Block until TRANSLATION_Y finishes. + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_Y) + + // The view should have been moved. + assertEquals(10f, testView.translationX, 1f) + assertEquals(500f, testView.translationY, 1f) + + // The end listener should have been called, this time with TRANSLATION_Y, y = 50, and + // allEnded = true. + verify(mockEndListener).onAnimationEnd( + testView, + DynamicAnimation.TRANSLATION_Y, + wasFling = false, + canceled = false, + finalValue = 500f, + finalVelocity = 0f, + allRelevantPropertyAnimsEnded = true) + verifyNoMoreInteractions(mockEndListener) + + // Now that all properties are done animating, the end action should have been called. + verify(mockEndAction, times(1)).run() + } + + @Test + fun testUpdateListeners() { + animator + .spring(DynamicAnimation.TRANSLATION_X, 100f, springConfig) + .spring(DynamicAnimation.TRANSLATION_Y, 50f, springConfig) + .addUpdateListener(object : UpdateListener<View> { + override fun onAnimationUpdateForProperty( + target: View, + values: UpdateMap<View> + ) { + mockUpdateListener.onAnimationUpdateForProperty(target, values) + } + }) + .start() + + verifyUpdateListenerCalls(animator, mockUpdateListener) + } + + @Test + fun testListenersNotCalledOnSubsequentAnimations() { + animator + .spring(DynamicAnimation.TRANSLATION_X, 10f, springConfig) + .addUpdateListener(mockUpdateListener) + .addEndListener(mockEndListener) + .withEndActions(mockEndAction::run) + .start() + + verifyUpdateListenerCalls(animator, mockUpdateListener) + verify(mockEndListener, times(1)).onAnimationEnd( + eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(false), eq(false), anyFloat(), + anyFloat(), eq(true)) + verify(mockEndAction, times(1)).run() + + animator + .spring(DynamicAnimation.TRANSLATION_X, 0f, springConfig) + .start() + + // We didn't pass any of the listeners/actions to the subsequent animation, so they should + // never have been called. + verifyNoMoreInteractions(mockUpdateListener) + verifyNoMoreInteractions(mockEndListener) + verifyNoMoreInteractions(mockEndAction) + } + + @Test + @Throws(InterruptedException::class) + fun testAnimationsUpdatedWhileInMotion() { + PhysicsAnimatorTestUtils.setAllAnimationsBlock(false) + + // Spring towards x = 100f. + animator + .spring( + DynamicAnimation.TRANSLATION_X, + 100f, + springConfig) + .start() + + // Block until it reaches x = 50f. + PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue( + animator) { view -> view.translationX > 50f } + + // Translation X value at the time of reversing the animation to spring to x = 0f. + val reversalTranslationX = testView.translationX + + // Spring back towards 0f. + animator + .spring( + DynamicAnimation.TRANSLATION_X, + 0f, + // Lower the stiffness to ensure the update listener receives at least one + // update frame where the view has continued to move to the right. + springConfig.apply { stiffness = SpringForce.STIFFNESS_LOW }) + .start() + + // Wait for TRANSLATION_X. + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_X) + + // Verify that the animation continued past the X value at the time of reversal, before + // springing back. This ensures the change in direction was not abrupt. + verifyAnimationUpdateFrames( + animator, DynamicAnimation.TRANSLATION_X, + { u -> u.value > reversalTranslationX }, + { u -> u.value < reversalTranslationX }) + + // Verify that the view is where it should be. + assertEquals(0f, testView.translationX, 1f) + } + + @Test + @Throws(InterruptedException::class) + @Ignore("Sporadically flaking.") + fun testAnimationsUpdatedWhileInMotion_originalListenersStillCalled() { + PhysicsAnimatorTestUtils.setAllAnimationsBlock(false) + + // Spring TRANSLATION_X to 100f, with an update and end listener provided. + animator + .spring( + DynamicAnimation.TRANSLATION_X, + 100f, + // Use very low stiffness to ensure that all of the keyframes we're testing + // for are reported to the update listener. + springConfig.apply { stiffness = SpringForce.STIFFNESS_VERY_LOW }) + .addUpdateListener(mockUpdateListener) + .addEndListener(mockEndListener) + .start() + + // Wait until the animation is halfway there. + PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue( + animator) { view -> view.translationX > 50f } + + // The end listener shouldn't have been called since the animation hasn't ended. + verifyNoMoreInteractions(mockEndListener) + + // Make sure we called the update listener with appropriate values. + verifyAnimationUpdateFrames(animator, DynamicAnimation.TRANSLATION_X, + { u -> u.value > 0f }, + { u -> u.value >= 50f }) + + // Mock a second end listener. + val secondEndListener = mock(EndListener::class.java) as EndListener<View> + val secondUpdateListener = mock(UpdateListener::class.java) as UpdateListener<View> + + // Start a new animation that springs both TRANSLATION_X and TRANSLATION_Y, and provide it + // the second end listener. This new end listener should be called for the end of + // TRANSLATION_X and TRANSLATION_Y, with allEnded = true when both have ended. + animator + .spring(DynamicAnimation.TRANSLATION_X, 200f, springConfig) + .spring(DynamicAnimation.TRANSLATION_Y, 4000f, springConfig) + .addUpdateListener(secondUpdateListener) + .addEndListener(secondEndListener) + .start() + + // Wait for TRANSLATION_X to end. + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_X) + + // The update listener provided to the initial animation call (the one that only animated + // TRANSLATION_X) should have been called with values on the way to x = 200f. This is + // because the second animation call updated the original TRANSLATION_X animation. + verifyAnimationUpdateFrames( + animator, DynamicAnimation.TRANSLATION_X, + { u -> u.value > 100f }, { u -> u.value >= 200f }) + + // The original end listener should also have been called, with allEnded = true since it was + // provided to an animator that animated only TRANSLATION_X. + verify(mockEndListener, times(1)) + .onAnimationEnd( + testView, DynamicAnimation.TRANSLATION_X, + wasFling = false, + canceled = false, + finalValue = 200f, + finalVelocity = 0f, + allRelevantPropertyAnimsEnded = true) + verifyNoMoreInteractions(mockEndListener) + + // The second end listener should have been called, but with allEnded = false since it was + // provided to an animator that animated both TRANSLATION_X and TRANSLATION_Y. + verify(secondEndListener, times(1)) + .onAnimationEnd(testView, DynamicAnimation.TRANSLATION_X, + wasFling = false, + canceled = false, + finalValue = 200f, + finalVelocity = 0f, + allRelevantPropertyAnimsEnded = false) + verifyNoMoreInteractions(secondEndListener) + + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_Y) + + // The original end listener shouldn't receive any callbacks because it was not provided to + // an animator that animated TRANSLATION_Y. + verifyNoMoreInteractions(mockEndListener) + + verify(secondEndListener, times(1)) + .onAnimationEnd(testView, DynamicAnimation.TRANSLATION_Y, + wasFling = false, + canceled = false, + finalValue = 4000f, + finalVelocity = 0f, + allRelevantPropertyAnimsEnded = true) + verifyNoMoreInteractions(secondEndListener) + } + + @Test + fun testFlingRespectsMinMax() { + animator + .fling(DynamicAnimation.TRANSLATION_X, + startVelocity = 1000f, + friction = 1.1f, + max = 10f) + .addEndListener(mockEndListener) + .start() + + // Ensure that the view stopped at x = 10f, and the end listener was called once with that + // value. + assertEquals(10f, testView.translationX, 1f) + verify(mockEndListener, times(1)) + .onAnimationEnd( + eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(true), eq(false), + eq(10f), anyFloat(), eq(true)) + + animator + .fling( + DynamicAnimation.TRANSLATION_X, + startVelocity = -1000f, + friction = 1.1f, + min = -5f) + .addEndListener(mockEndListener) + .start() + + // Ensure that the view stopped at x = -5f, and the end listener was called once with that + // value. + assertEquals(-5f, testView.translationX, 1f) + verify(mockEndListener, times(1)) + .onAnimationEnd( + eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(true), eq(false), + eq(-5f), anyFloat(), eq(true)) + } + + @Test + fun testIsPropertyAnimating() { + PhysicsAnimatorTestUtils.setAllAnimationsBlock(false) + + testView.physicsAnimator + .spring(DynamicAnimation.TRANSLATION_X, 500f, springConfig) + .fling(DynamicAnimation.TRANSLATION_Y, 10f, flingConfig) + .spring(DynamicAnimation.TRANSLATION_Z, 1000f, springConfig) + .start() + + // All of the properties we just started should be animating. + assertTrue(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_X)) + assertTrue(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Y)) + assertTrue(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Z)) + + // Block until x and y end. + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(testView.physicsAnimator, + DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) + + // Verify that x and y are no longer animating, but that Z is (it's springing to 1000f). + assertFalse(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_X)) + assertFalse(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Y)) + assertTrue(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Z)) + + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Z) + + assertFalse(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_X)) + assertFalse(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Y)) + assertFalse(testView.physicsAnimator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Z)) + } + + @Test + fun testExtensionProperty() { + testView + .physicsAnimator + .spring(DynamicAnimation.TRANSLATION_X, 200f) + .start() + + assertEquals(200f, testView.translationX, 1f) + } + + @Test + @Ignore("Sporadically flaking.") + fun testFlingThenSpring() { + PhysicsAnimatorTestUtils.setAllAnimationsBlock(false) + + // Start at 500f and fling hard to the left. We should quickly reach the 250f minimum, fly + // past it since there's so much velocity remaining, then spring back to 250f. + testView.translationX = 500f + animator + .flingThenSpring( + DynamicAnimation.TRANSLATION_X, + -5000f, + flingConfig.apply { min = 250f }, + springConfig) + .addUpdateListener(mockUpdateListener) + .addEndListener(mockEndListener) + .withEndActions(mockEndAction::run) + .start() + + // Block until we pass the minimum. + PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue( + animator) { v -> v.translationX <= 250f } + + // Double check that the view is there. + assertTrue(testView.translationX <= 250f) + + // The update listener should have been called with a value < 500f, and then a value less + // than or equal to the 250f minimum. + verifyAnimationUpdateFrames(animator, DynamicAnimation.TRANSLATION_X, + { u -> u.value < 500f }, + { u -> u.value <= 250f }) + + // Despite the fact that the fling has ended, the end listener shouldn't have been called + // since we're about to begin springing the same property. + verifyNoMoreInteractions(mockEndListener) + verifyNoMoreInteractions(mockEndAction) + + // Wait for the spring to finish. + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_X) + + // Make sure we continued past 250f since the spring should have been started with some + // remaining negative velocity from the fling. + verifyAnimationUpdateFrames(animator, DynamicAnimation.TRANSLATION_X, + { u -> u.value < 250f }) + + // At this point, the animation end listener should have been called once, and only once, + // when the spring ended at 250f. + verify(mockEndListener).onAnimationEnd(testView, DynamicAnimation.TRANSLATION_X, + wasFling = false, + canceled = false, + finalValue = 250f, + finalVelocity = 0f, + allRelevantPropertyAnimsEnded = true) + verifyNoMoreInteractions(mockEndListener) + + // The end action should also have been called once. + verify(mockEndAction, times(1)).run() + verifyNoMoreInteractions(mockEndAction) + + assertEquals(250f, testView.translationX) + } + + @Test + fun testFlingThenSpring_objectOutsideFlingBounds() { + // Start the view at x = -500, well outside the fling bounds of min = 0f, with strong + // negative velocity. + testView.translationX = -500f + animator + .flingThenSpring( + DynamicAnimation.TRANSLATION_X, + -5000f, + flingConfig.apply { min = 0f }, + springConfig) + .addUpdateListener(mockUpdateListener) + .addEndListener(mockEndListener) + .withEndActions(mockEndAction::run) + .start() + + // The initial -5000f velocity should result in frames to the left of -500f before the view + // springs back towards 0f. + verifyAnimationUpdateFrames( + animator, DynamicAnimation.TRANSLATION_X, + { u -> u.value < -500f }, + { u -> u.value > -500f }) + + // We should end up at the fling min. + assertEquals(0f, testView.translationX, 1f) + } + + @Test + fun testFlingToMinMaxThenSpring() { + // Start at x = 500f. + testView.translationX = 500f + + // Fling to the left at the very sad rate of -1 pixels per second. That won't get us much of + // anywhere, and certainly not to the 0f min. + animator + // Good thing we have flingToMinMaxThenSpring! + .flingThenSpring( + DynamicAnimation.TRANSLATION_X, + -10000f, + flingConfig.apply { min = 0f }, + springConfig, + flingMustReachMinOrMax = true) + .addUpdateListener(mockUpdateListener) + .addEndListener(mockEndListener) + .withEndActions(mockEndAction::run) + .start() + + // Thanks, flingToMinMaxThenSpring, for adding enough velocity to get us here. + assertEquals(0f, testView.translationX, 1f) + } + + /** + * Verifies that the calls to the mock update listener match the animation update frames + * reported by the test internal listener, in order. + */ + private fun <T : Any> verifyUpdateListenerCalls( + animator: PhysicsAnimator<T>, + mockUpdateListener: UpdateListener<T> + ) { + val updates = getAnimationUpdateFrames(animator) + + for (invocation in Mockito.mockingDetails(mockUpdateListener).invocations) { + + // Grab the update map of Property -> AnimationUpdate that was passed to the mock update + // listener. + val updateMap = invocation.arguments[1] + as ArrayMap<FloatPropertyCompat<in T>, PhysicsAnimator.AnimationUpdate> + + // + for ((property, update) in updateMap) { + val updatesForProperty = updates[property]!! + + // This update should be the next one in the list for this property. + if (update != updatesForProperty[0]) { + Assert.fail("The update listener was called with an unexpected value: $update.") + } + + updatesForProperty.remove(update) + } + + val target = animator.weakTarget.get() + assertNotNull(target) + // Mark this invocation verified. + verify(mockUpdateListener).onAnimationUpdateForProperty(target!!, updateMap) + } + + verifyNoMoreInteractions(mockUpdateListener) + + // Since we were removing values as matching invocations were found, there should no longer + // be any values remaining. If there are, it means the update listener wasn't notified when + // it should have been. + assertEquals(0, + updates.values.fold(0, { count, propertyUpdates -> count + propertyUpdates.size })) + + clearAnimationUpdateFrames(animator) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java new file mode 100644 index 000000000000..080cddc58a09 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java @@ -0,0 +1,86 @@ +/* + * 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 static android.view.Display.DEFAULT_DISPLAY; +import static android.view.InsetsState.ITYPE_IME; +import static android.view.Surface.ROTATION_0; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +import android.graphics.Point; +import android.view.InsetsSourceControl; +import android.view.InsetsState; +import android.view.SurfaceControl; + +import androidx.test.filters.SmallTest; + +import com.android.internal.view.IInputMethodManager; + +import org.junit.Before; +import org.junit.Test; + +@SmallTest +public class DisplayImeControllerTest { + + private SurfaceControl.Transaction mT; + private DisplayImeController.PerDisplay mPerDisplay; + private IInputMethodManager mMock; + + @Before + public void setUp() throws Exception { + mT = mock(SurfaceControl.Transaction.class); + mMock = mock(IInputMethodManager.class); + mPerDisplay = new DisplayImeController(null, null, Runnable::run, new TransactionPool() { + @Override + public SurfaceControl.Transaction acquire() { + return mT; + } + + @Override + public void release(SurfaceControl.Transaction t) { + } + }) { + @Override + public IInputMethodManager getImms() { + return mMock; + } + }.new PerDisplay(DEFAULT_DISPLAY, ROTATION_0); + } + + @Test + public void reappliesVisibilityToChangedLeash() { + verifyZeroInteractions(mT); + + mPerDisplay.mImeShowing = false; + mPerDisplay.insetsControlChanged(new InsetsState(), new InsetsSourceControl[] { + new InsetsSourceControl(ITYPE_IME, mock(SurfaceControl.class), new Point(0, 0)) + }); + + verify(mT).hide(any()); + + mPerDisplay.mImeShowing = true; + mPerDisplay.insetsControlChanged(new InsetsState(), new InsetsSourceControl[] { + new InsetsSourceControl(ITYPE_IME, mock(SurfaceControl.class), new Point(0, 0)) + }); + + verify(mT).show(any()); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java new file mode 100644 index 000000000000..2b5b77e49e3a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2019 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 static android.content.res.Configuration.UI_MODE_TYPE_NORMAL; +import static android.view.Surface.ROTATION_0; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Insets; +import android.graphics.Rect; +import android.view.DisplayCutout; +import android.view.DisplayInfo; + +import androidx.test.filters.SmallTest; + +import com.android.internal.R; + +import org.junit.Test; + +@SmallTest +public class DisplayLayoutTest { + + @Test + public void testInsets() { + Resources res = createResources(40, 50, false, 30, 40); + // Test empty display, no bars or anything + DisplayInfo info = createDisplayInfo(1000, 1500, 0, ROTATION_0); + DisplayLayout dl = new DisplayLayout(info, res, false, false); + assertEquals(new Rect(0, 0, 0, 0), dl.stableInsets()); + assertEquals(new Rect(0, 0, 0, 0), dl.nonDecorInsets()); + + // Test with bars + dl = new DisplayLayout(info, res, true, true); + assertEquals(new Rect(0, 40, 0, 50), dl.stableInsets()); + assertEquals(new Rect(0, 0, 0, 50), dl.nonDecorInsets()); + + // Test just cutout + info = createDisplayInfo(1000, 1500, 60, ROTATION_0); + dl = new DisplayLayout(info, res, false, false); + assertEquals(new Rect(0, 60, 0, 0), dl.stableInsets()); + assertEquals(new Rect(0, 60, 0, 0), dl.nonDecorInsets()); + + // Test with bars and cutout + dl = new DisplayLayout(info, res, true, true); + assertEquals(new Rect(0, 60, 0, 50), dl.stableInsets()); + assertEquals(new Rect(0, 60, 0, 50), dl.nonDecorInsets()); + } + + @Test + public void testRotate() { + // Basic rotate utility + Rect testParent = new Rect(0, 0, 1000, 600); + Rect testInner = new Rect(40, 20, 120, 80); + Rect testResult = new Rect(testInner); + DisplayLayout.rotateBounds(testResult, testParent, 1); + assertEquals(new Rect(20, 880, 80, 960), testResult); + testResult.set(testInner); + DisplayLayout.rotateBounds(testResult, testParent, 2); + assertEquals(new Rect(880, 20, 960, 80), testResult); + testResult.set(testInner); + DisplayLayout.rotateBounds(testResult, testParent, 3); + assertEquals(new Rect(520, 40, 580, 120), testResult); + + Resources res = createResources(40, 50, false, 30, 40); + DisplayInfo info = createDisplayInfo(1000, 1500, 60, ROTATION_0); + DisplayLayout dl = new DisplayLayout(info, res, true, true); + assertEquals(new Rect(0, 60, 0, 50), dl.stableInsets()); + assertEquals(new Rect(0, 60, 0, 50), dl.nonDecorInsets()); + + // Rotate to 90 + dl.rotateTo(res, ROTATION_90); + assertEquals(new Rect(60, 30, 0, 40), dl.stableInsets()); + assertEquals(new Rect(60, 0, 0, 40), dl.nonDecorInsets()); + + // Rotate with moving navbar + res = createResources(40, 50, true, 30, 40); + dl = new DisplayLayout(info, res, true, true); + dl.rotateTo(res, ROTATION_270); + assertEquals(new Rect(40, 30, 60, 0), dl.stableInsets()); + assertEquals(new Rect(40, 0, 60, 0), dl.nonDecorInsets()); + } + + private Resources createResources( + int navLand, int navPort, boolean navMoves, int statusLand, int statusPort) { + Configuration cfg = new Configuration(); + cfg.uiMode = UI_MODE_TYPE_NORMAL; + Resources res = mock(Resources.class); + doReturn(navLand).when(res).getDimensionPixelSize( + R.dimen.navigation_bar_height_landscape_car_mode); + doReturn(navPort).when(res).getDimensionPixelSize(R.dimen.navigation_bar_height_car_mode); + doReturn(navLand).when(res).getDimensionPixelSize(R.dimen.navigation_bar_width_car_mode); + doReturn(navLand).when(res).getDimensionPixelSize(R.dimen.navigation_bar_height_landscape); + doReturn(navPort).when(res).getDimensionPixelSize(R.dimen.navigation_bar_height); + doReturn(navLand).when(res).getDimensionPixelSize(R.dimen.navigation_bar_width); + doReturn(navMoves).when(res).getBoolean(R.bool.config_navBarCanMove); + doReturn(statusLand).when(res).getDimensionPixelSize(R.dimen.status_bar_height_landscape); + doReturn(statusPort).when(res).getDimensionPixelSize(R.dimen.status_bar_height_portrait); + doReturn(cfg).when(res).getConfiguration(); + return res; + } + + private DisplayInfo createDisplayInfo(int width, int height, int cutoutHeight, int rotation) { + DisplayInfo info = new DisplayInfo(); + info.logicalWidth = width; + info.logicalHeight = height; + info.rotation = rotation; + if (cutoutHeight > 0) { + info.displayCutout = new DisplayCutout( + Insets.of(0, cutoutHeight, 0, 0) /* safeInsets */, null /* boundLeft */, + new Rect(width / 2 - cutoutHeight, 0, width / 2 + cutoutHeight, + cutoutHeight) /* boundTop */, null /* boundRight */, + null /* boundBottom */); + } else { + info.displayCutout = DisplayCutout.NO_CUTOUT; + } + info.logicalDensityDpi = 300; + return info; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt new file mode 100644 index 000000000000..fe536411d5ed --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt @@ -0,0 +1,454 @@ +/* + * 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.magnetictarget + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.MotionEvent +import android.view.View +import androidx.dynamicanimation.animation.FloatPropertyCompat +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.animation.PhysicsAnimatorTestUtils +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.anyFloat +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions + +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner::class) +@SmallTest +class MagnetizedObjectTest : SysuiTestCase() { + /** Incrementing value for fake MotionEvent timestamps. */ + private var time = 0L + + /** Value to add to each new MotionEvent's timestamp. */ + private var timeStep = 100 + + private val underlyingObject = this + + private lateinit var targetView: View + + private val targetSize = 200 + private val targetCenterX = 500 + private val targetCenterY = 900 + private val magneticFieldRadius = 200 + + private var objectX = 0f + private var objectY = 0f + private val objectSize = 50f + + private lateinit var magneticTarget: MagnetizedObject.MagneticTarget + private lateinit var magnetizedObject: MagnetizedObject<*> + private lateinit var magnetListener: MagnetizedObject.MagnetListener + + private val xProperty = object : FloatPropertyCompat<MagnetizedObjectTest>("") { + override fun setValue(target: MagnetizedObjectTest?, value: Float) { + objectX = value + } + override fun getValue(target: MagnetizedObjectTest?): Float { + return objectX + } + } + + private val yProperty = object : FloatPropertyCompat<MagnetizedObjectTest>("") { + override fun setValue(target: MagnetizedObjectTest?, value: Float) { + objectY = value + } + + override fun getValue(target: MagnetizedObjectTest?): Float { + return objectY + } + } + + @Before + fun setup() { + PhysicsAnimatorTestUtils.prepareForTest() + + // Mock the view since a real view's getLocationOnScreen() won't work unless it's attached + // to a real window (it'll always return x = 0, y = 0). + targetView = mock(View::class.java) + `when`(targetView.context).thenReturn(context) + + // The mock target view will pretend that it's 200x200, and at (400, 800). This means it's + // occupying the bounds (400, 800, 600, 1000) and it has a center of (500, 900). + `when`(targetView.width).thenReturn(targetSize) // width = 200 + `when`(targetView.height).thenReturn(targetSize) // height = 200 + doAnswer { invocation -> + (invocation.arguments[0] as IntArray).also { location -> + // Return the top left of the target. + location[0] = targetCenterX - targetSize / 2 // x = 400 + location[1] = targetCenterY - targetSize / 2 // y = 800 + } + }.`when`(targetView).getLocationOnScreen(ArgumentMatchers.any()) + doAnswer { invocation -> + (invocation.arguments[0] as Runnable).run() + true + }.`when`(targetView).post(ArgumentMatchers.any()) + `when`(targetView.context).thenReturn(context) + + magneticTarget = MagnetizedObject.MagneticTarget(targetView, magneticFieldRadius) + + magnetListener = mock(MagnetizedObject.MagnetListener::class.java) + magnetizedObject = object : MagnetizedObject<MagnetizedObjectTest>( + context, underlyingObject, xProperty, yProperty) { + override fun getWidth(underlyingObject: MagnetizedObjectTest): Float { + return objectSize + } + + override fun getHeight(underlyingObject: MagnetizedObjectTest): Float { + return objectSize + } + + override fun getLocationOnScreen( + underlyingObject: MagnetizedObjectTest, + loc: IntArray + ) { + loc[0] = objectX.toInt() + loc[1] = objectY.toInt() } + } + + magnetizedObject.magnetListener = magnetListener + magnetizedObject.addTarget(magneticTarget) + + timeStep = 100 + } + + @Test + fun testMotionEventConsumption() { + // Start at (0, 0). No magnetic field here. + assertFalse(magnetizedObject.maybeConsumeMotionEvent(getMotionEvent( + x = 0, y = 0, action = MotionEvent.ACTION_DOWN))) + + // Move to (400, 400), which is solidly outside the magnetic field. + assertFalse(magnetizedObject.maybeConsumeMotionEvent(getMotionEvent( + x = 200, y = 200))) + + // Move to (305, 705). This would be in the magnetic field radius if magnetic fields were + // square. It's not, because they're not. + assertFalse(magnetizedObject.maybeConsumeMotionEvent(getMotionEvent( + x = targetCenterX - magneticFieldRadius + 5, + y = targetCenterY - magneticFieldRadius + 5))) + + // Move to (400, 800). That's solidly in the radius so the magnetic target should begin + // consuming events. + assertTrue(magnetizedObject.maybeConsumeMotionEvent(getMotionEvent( + x = targetCenterX - 100, + y = targetCenterY - 100))) + + // Release at (400, 800). Since we're in the magnetic target, it should return true and + // consume the ACTION_UP. + assertTrue(magnetizedObject.maybeConsumeMotionEvent(getMotionEvent( + x = 400, y = 800, action = MotionEvent.ACTION_UP))) + + // ACTION_DOWN outside the field. + assertFalse(magnetizedObject.maybeConsumeMotionEvent(getMotionEvent( + x = 200, y = 200, action = MotionEvent.ACTION_DOWN))) + + // Move to the center. We absolutely should consume events there. + assertTrue(magnetizedObject.maybeConsumeMotionEvent(getMotionEvent( + x = targetCenterX, + y = targetCenterY))) + + // Drag out to (0, 0) and we should be returning false again. + assertFalse(magnetizedObject.maybeConsumeMotionEvent(getMotionEvent( + x = 0, y = 0))) + + // The ACTION_UP event shouldn't be consumed either since it's outside the field. + assertFalse(magnetizedObject.maybeConsumeMotionEvent(getMotionEvent( + x = 0, y = 0, action = MotionEvent.ACTION_UP))) + } + + @Test + fun testMotionEventConsumption_downInMagneticField() { + // We should not consume DOWN events even if they occur in the field. + assertFalse(magnetizedObject.maybeConsumeMotionEvent(getMotionEvent( + x = targetCenterX, y = targetCenterY, action = MotionEvent.ACTION_DOWN))) + } + + @Test + fun testMoveIntoAroundAndOutOfMagneticField() { + // Move around but don't touch the magnetic field. + dispatchMotionEvents( + getMotionEvent(x = 0, y = 0, action = MotionEvent.ACTION_DOWN), + getMotionEvent(x = 100, y = 100), + getMotionEvent(x = 200, y = 200)) + + // You can't become unstuck if you were never stuck in the first place. + verify(magnetListener, never()).onStuckToTarget(magneticTarget) + verify(magnetListener, never()).onUnstuckFromTarget( + eq(magneticTarget), ArgumentMatchers.anyFloat(), ArgumentMatchers.anyFloat(), + eq(false)) + + // Move into and then around inside the magnetic field. + dispatchMotionEvents( + getMotionEvent(x = targetCenterX - 100, y = targetCenterY - 100), + getMotionEvent(x = targetCenterX, y = targetCenterY), + getMotionEvent(x = targetCenterX + 100, y = targetCenterY + 100)) + + // We should only have received one call to onStuckToTarget and none to unstuck. + verify(magnetListener, times(1)).onStuckToTarget(magneticTarget) + verify(magnetListener, never()).onUnstuckFromTarget( + eq(magneticTarget), ArgumentMatchers.anyFloat(), ArgumentMatchers.anyFloat(), + eq(false)) + + // Move out of the field and then release. + dispatchMotionEvents( + getMotionEvent(x = 100, y = 100), + getMotionEvent(x = 100, y = 100, action = MotionEvent.ACTION_UP)) + + // We should have received one unstuck call and no more stuck calls. We also should never + // have received an onReleasedInTarget call. + verify(magnetListener, times(1)).onUnstuckFromTarget( + eq(magneticTarget), ArgumentMatchers.anyFloat(), ArgumentMatchers.anyFloat(), + eq(false)) + verifyNoMoreInteractions(magnetListener) + } + + @Test + fun testMoveIntoOutOfAndBackIntoMagneticField() { + // Move into the field + dispatchMotionEvents( + getMotionEvent( + x = targetCenterX - magneticFieldRadius, + y = targetCenterY - magneticFieldRadius, + action = MotionEvent.ACTION_DOWN), + getMotionEvent( + x = targetCenterX, y = targetCenterY)) + + verify(magnetListener, times(1)).onStuckToTarget(magneticTarget) + verify(magnetListener, never()).onReleasedInTarget(magneticTarget) + + // Move back out. + dispatchMotionEvents( + getMotionEvent( + x = targetCenterX - magneticFieldRadius, + y = targetCenterY - magneticFieldRadius)) + + verify(magnetListener, times(1)).onUnstuckFromTarget( + eq(magneticTarget), ArgumentMatchers.anyFloat(), ArgumentMatchers.anyFloat(), + eq(false)) + verify(magnetListener, never()).onReleasedInTarget(magneticTarget) + + // Move in again and release in the magnetic field. + dispatchMotionEvents( + getMotionEvent(x = targetCenterX - 100, y = targetCenterY - 100), + getMotionEvent(x = targetCenterX + 50, y = targetCenterY + 50), + getMotionEvent(x = targetCenterX, y = targetCenterY), + getMotionEvent( + x = targetCenterX, y = targetCenterY, action = MotionEvent.ACTION_UP)) + + verify(magnetListener, times(2)).onStuckToTarget(magneticTarget) + verify(magnetListener).onReleasedInTarget(magneticTarget) + verifyNoMoreInteractions(magnetListener) + } + + @Test + fun testFlingTowardsTarget_towardsTarget() { + timeStep = 10 + + // Forcefully fling the object towards the target (but never touch the magnetic field). + dispatchMotionEvents( + getMotionEvent( + x = targetCenterX, + y = 0, + action = MotionEvent.ACTION_DOWN), + getMotionEvent( + x = targetCenterX, + y = targetCenterY / 2), + getMotionEvent( + x = targetCenterX, + y = targetCenterY - magneticFieldRadius * 2, + action = MotionEvent.ACTION_UP)) + + // Nevertheless it should have ended up stuck to the target. + verify(magnetListener, times(1)).onStuckToTarget(magneticTarget) + } + + @Test + fun testFlingTowardsTarget_towardsButTooSlow() { + // Very, very slowly fling the object towards the target (but never touch the magnetic + // field). This value is only used to create MotionEvent timestamps, it will not block the + // test for 10 seconds. + timeStep = 10000 + dispatchMotionEvents( + getMotionEvent( + x = targetCenterX, + y = 0, + action = MotionEvent.ACTION_DOWN), + getMotionEvent( + x = targetCenterX, + y = targetCenterY / 2), + getMotionEvent( + x = targetCenterX, + y = targetCenterY - magneticFieldRadius * 2, + action = MotionEvent.ACTION_UP)) + + // No sticking should have occurred. + verifyNoMoreInteractions(magnetListener) + } + + @Test + fun testFlingTowardsTarget_missTarget() { + timeStep = 10 + // Forcefully fling the object down, but not towards the target. + dispatchMotionEvents( + getMotionEvent( + x = 0, + y = 0, + action = MotionEvent.ACTION_DOWN), + getMotionEvent( + x = 0, + y = targetCenterY / 2), + getMotionEvent( + x = 0, + y = targetCenterY - magneticFieldRadius * 2, + action = MotionEvent.ACTION_UP)) + + verifyNoMoreInteractions(magnetListener) + } + + @Test + fun testMagnetAnimation() { + // Make sure the object starts at (0, 0). + assertEquals(0f, objectX) + assertEquals(0f, objectY) + + // Trigger the magnet animation, and block the test until it ends. + PhysicsAnimatorTestUtils.setAllAnimationsBlock(true) + magnetizedObject.maybeConsumeMotionEvent(getMotionEvent( + x = targetCenterX - 250, + y = targetCenterY - 250, + action = MotionEvent.ACTION_DOWN)) + + magnetizedObject.maybeConsumeMotionEvent(getMotionEvent( + x = targetCenterX, + y = targetCenterY)) + + // The object's (top-left) position should now position it centered over the target. + assertEquals(targetCenterX - objectSize / 2, objectX) + assertEquals(targetCenterY - objectSize / 2, objectY) + } + + @Test + fun testMultipleTargets() { + val secondMagneticTarget = getSecondMagneticTarget() + + // Drag into the second target. + dispatchMotionEvents( + getMotionEvent(x = 0, y = 0, action = MotionEvent.ACTION_DOWN), + getMotionEvent(x = 100, y = 900)) + + // Verify that we received an onStuck for the second target, and no others. + verify(magnetListener).onStuckToTarget(secondMagneticTarget) + verifyNoMoreInteractions(magnetListener) + + // Drag into the original target. + dispatchMotionEvents( + getMotionEvent(x = 0, y = 0), + getMotionEvent(x = 500, y = 900)) + + // We should have unstuck from the second one and stuck into the original one. + verify(magnetListener).onUnstuckFromTarget( + eq(secondMagneticTarget), anyFloat(), anyFloat(), eq(false)) + verify(magnetListener).onStuckToTarget(magneticTarget) + verifyNoMoreInteractions(magnetListener) + } + + @Test + fun testMultipleTargets_flingIntoSecond() { + val secondMagneticTarget = getSecondMagneticTarget() + + timeStep = 10 + + // Fling towards the second target. + dispatchMotionEvents( + getMotionEvent(x = 100, y = 0, action = MotionEvent.ACTION_DOWN), + getMotionEvent(x = 100, y = 350), + getMotionEvent(x = 100, y = 650, action = MotionEvent.ACTION_UP)) + + // Verify that we received an onStuck for the second target. + verify(magnetListener).onStuckToTarget(secondMagneticTarget) + + // Fling towards the first target. + dispatchMotionEvents( + getMotionEvent(x = 300, y = 0, action = MotionEvent.ACTION_DOWN), + getMotionEvent(x = 400, y = 350), + getMotionEvent(x = 500, y = 650, action = MotionEvent.ACTION_UP)) + + // Verify that we received onStuck for the original target. + verify(magnetListener).onStuckToTarget(magneticTarget) + } + + private fun getSecondMagneticTarget(): MagnetizedObject.MagneticTarget { + // The first target view is at bounds (400, 800, 600, 1000) and it has a center of + // (500, 900). We'll add a second one at bounds (0, 800, 200, 1000) with center (100, 900). + val secondTargetView = mock(View::class.java) + var secondTargetCenterX = 100 + var secondTargetCenterY = 900 + + `when`(secondTargetView.context).thenReturn(context) + `when`(secondTargetView.width).thenReturn(targetSize) // width = 200 + `when`(secondTargetView.height).thenReturn(targetSize) // height = 200 + doAnswer { invocation -> + (invocation.arguments[0] as Runnable).run() + true + }.`when`(secondTargetView).post(ArgumentMatchers.any()) + doAnswer { invocation -> + (invocation.arguments[0] as IntArray).also { location -> + // Return the top left of the target. + location[0] = secondTargetCenterX - targetSize / 2 // x = 0 + location[1] = secondTargetCenterY - targetSize / 2 // y = 800 + } + }.`when`(secondTargetView).getLocationOnScreen(ArgumentMatchers.any()) + + return magnetizedObject.addTarget(secondTargetView, magneticFieldRadius) + } + + /** + * Return a MotionEvent at the given coordinates, with the given action (or MOVE by default). + * The event's time fields will be incremented by 10ms each time this is called, so tha + * VelocityTracker works. + */ + private fun getMotionEvent( + x: Int, + y: Int, + action: Int = MotionEvent.ACTION_MOVE + ): MotionEvent { + return MotionEvent.obtain(time, time, action, x.toFloat(), y.toFloat(), 0) + .also { time += timeStep } + } + + /** Dispatch all of the provided events to the target view. */ + private fun dispatchMotionEvents(vararg events: MotionEvent) { + events.forEach { magnetizedObject.maybeConsumeMotionEvent(it) } + } + + /** Prevents Kotlin from being mad that eq() is nullable. */ + private fun <T> eq(value: T): T = Mockito.eq(value) ?: value +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedAnimationControllerTest.java new file mode 100644 index 000000000000..a8a3a9fd7da2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedAnimationControllerTest.java @@ -0,0 +1,71 @@ +/* + * 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.onehanded; + +import static org.junit.Assert.assertNotNull; + +import android.graphics.Rect; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.SurfaceControl; + +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; + +/** + * Unit tests against {@link OneHandedAnimationController} to ensure that it sends the right + * callbacks + * depending on the various interactions. + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class OneHandedAnimationControllerTest extends OneHandedTestCase { + private static final int TEST_BOUNDS_WIDTH = 1000; + private static final int TEST_BOUNDS_HEIGHT = 1000; + + OneHandedAnimationController mOneHandedAnimationController; + OneHandedTutorialHandler mTutorialHandler; + + @Mock + private SurfaceControl mMockLeash; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mTutorialHandler = new OneHandedTutorialHandler(mContext); + mOneHandedAnimationController = new OneHandedAnimationController(mContext); + } + + @Test + public void testGetAnimator_withSameBounds_returnAnimator() { + final Rect originalBounds = new Rect(0, 0, TEST_BOUNDS_WIDTH, TEST_BOUNDS_HEIGHT); + final Rect destinationBounds = originalBounds; + destinationBounds.offset(0, 300); + final OneHandedAnimationController.OneHandedTransitionAnimator animator = + mOneHandedAnimationController + .getAnimator(mMockLeash, originalBounds, destinationBounds); + + assertNotNull(animator); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java new file mode 100644 index 000000000000..3645f1e56f92 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java @@ -0,0 +1,193 @@ +/* + * 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.onehanded; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.om.IOverlayManager; +import android.provider.Settings; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.Display; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.DisplayController; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class OneHandedControllerTest extends OneHandedTestCase { + Display mDisplay; + OneHandedController mOneHandedController; + OneHandedTimeoutHandler mTimeoutHandler; + + @Mock + DisplayController mMockDisplayController; + @Mock + OneHandedDisplayAreaOrganizer mMockDisplayAreaOrganizer; + @Mock + OneHandedTouchHandler mMockTouchHandler; + @Mock + OneHandedTutorialHandler mMockTutorialHandler; + @Mock + OneHandedGestureHandler mMockGestureHandler; + @Mock + OneHandedTimeoutHandler mMockTimeoutHandler; + @Mock + IOverlayManager mMockOverlayManager; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mDisplay = mContext.getDisplay(); + OneHandedController oneHandedController = new OneHandedController( + mContext, + mMockDisplayController, + mMockDisplayAreaOrganizer, + mMockTouchHandler, + mMockTutorialHandler, + mMockGestureHandler, + mMockOverlayManager); + mOneHandedController = Mockito.spy(oneHandedController); + mTimeoutHandler = Mockito.spy(OneHandedTimeoutHandler.get()); + + when(mMockDisplayController.getDisplay(anyInt())).thenReturn(mDisplay); + when(mMockDisplayAreaOrganizer.isInOneHanded()).thenReturn(false); + } + + @Test + public void testDefaultShouldNotInOneHanded() { + final OneHandedAnimationController animationController = new OneHandedAnimationController( + mContext); + OneHandedDisplayAreaOrganizer displayAreaOrganizer = new OneHandedDisplayAreaOrganizer( + mContext, mMockDisplayController, animationController, mMockTutorialHandler); + + assertThat(displayAreaOrganizer.isInOneHanded()).isFalse(); + } + + @Test + public void testRegisterOrganizer() { + verify(mMockDisplayAreaOrganizer, atLeastOnce()).registerOrganizer(anyInt()); + } + + @Test + public void testStartOneHanded() { + mOneHandedController.startOneHanded(); + + verify(mMockDisplayAreaOrganizer).scheduleOffset(anyInt(), anyInt()); + } + + @Test + public void testStopOneHanded() { + when(mMockDisplayAreaOrganizer.isInOneHanded()).thenReturn(false); + mOneHandedController.stopOneHanded(); + + verify(mMockDisplayAreaOrganizer, never()).scheduleOffset(anyInt(), anyInt()); + } + + @Test + public void testRegisterTransitionCallbackAfterInit() { + verify(mMockDisplayAreaOrganizer).registerTransitionCallback(mMockTouchHandler); + verify(mMockDisplayAreaOrganizer).registerTransitionCallback(mMockGestureHandler); + verify(mMockDisplayAreaOrganizer).registerTransitionCallback(mMockTutorialHandler); + } + + @Test + public void testRegisterTransitionCallback() { + OneHandedTransitionCallback callback = new OneHandedTransitionCallback() {}; + mOneHandedController.registerTransitionCallback(callback); + + verify(mMockDisplayAreaOrganizer).registerTransitionCallback(callback); + } + + + @Test + public void testStopOneHanded_shouldRemoveTimer() { + mOneHandedController.stopOneHanded(); + + verify(mTimeoutHandler).removeTimer(); + } + + @Test + public void testUpdateIsEnabled() { + final boolean enabled = true; + mOneHandedController.setOneHandedEnabled(enabled); + + verify(mMockTouchHandler, atLeastOnce()).onOneHandedEnabled(enabled); + } + + @Test + public void testUpdateSwipeToNotificationIsEnabled() { + final boolean enabled = true; + mOneHandedController.setSwipeToNotificationEnabled(enabled); + + verify(mMockTouchHandler, atLeastOnce()).onOneHandedEnabled(enabled); + } + + @Ignore("b/167943723, refactor it and fix it") + @Test + public void tesSettingsObserver_updateTapAppToExit() { + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.TAPS_APP_TO_EXIT, 1); + + verify(mOneHandedController).setTaskChangeToExit(true); + } + + @Ignore("b/167943723, refactor it and fix it") + @Test + public void tesSettingsObserver_updateEnabled() { + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.ONE_HANDED_MODE_ENABLED, 1); + + verify(mOneHandedController).setOneHandedEnabled(true); + } + + @Ignore("b/167943723, refactor it and fix it") + @Test + public void tesSettingsObserver_updateTimeout() { + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.ONE_HANDED_MODE_TIMEOUT, + OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS); + + verify(mMockTimeoutHandler).setTimeout( + OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS); + } + + @Ignore("b/167943723, refactor it and fix it") + @Test + public void tesSettingsObserver_updateSwipeToNotification() { + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED, 1); + + verify(mOneHandedController).setSwipeToNotificationEnabled(true); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java new file mode 100644 index 000000000000..6d1a3c472245 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java @@ -0,0 +1,321 @@ +/* + * 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.onehanded; + +import static android.view.Display.DEFAULT_DISPLAY; +import static android.window.DisplayAreaOrganizer.FEATURE_ONE_HANDED; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.res.Configuration; +import android.os.Handler; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.Display; +import android.view.Surface; +import android.view.SurfaceControl; +import android.window.DisplayAreaInfo; +import android.window.IWindowContainerToken; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.DisplayController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class OneHandedDisplayAreaOrganizerTest extends OneHandedTestCase { + static final int DISPLAY_WIDTH = 1000; + static final int DISPLAY_HEIGHT = 1000; + + DisplayAreaInfo mDisplayAreaInfo; + Display mDisplay; + OneHandedDisplayAreaOrganizer mDisplayAreaOrganizer; + OneHandedTutorialHandler mTutorialHandler; + OneHandedAnimationController.OneHandedTransitionAnimator mFakeAnimator; + WindowContainerToken mToken; + SurfaceControl mLeash; + TestableLooper mTestableLooper; + @Mock + IWindowContainerToken mMockRealToken; + @Mock + OneHandedAnimationController mMockAnimationController; + @Mock + OneHandedAnimationController.OneHandedTransitionAnimator mMockAnimator; + @Mock + OneHandedSurfaceTransactionHelper mMockSurfaceTransactionHelper; + @Mock + DisplayController mMockDisplayController; + @Mock + SurfaceControl mMockLeash; + @Mock + WindowContainerTransaction mMockWindowContainerTransaction; + + Handler mSpyUpdateHandler; + Handler.Callback mUpdateCallback = (msg) -> false; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mTestableLooper = TestableLooper.get(this); + mToken = new WindowContainerToken(mMockRealToken); + mLeash = new SurfaceControl(); + mDisplay = mContext.getDisplay(); + mDisplayAreaInfo = new DisplayAreaInfo(mToken, DEFAULT_DISPLAY, FEATURE_ONE_HANDED); + mDisplayAreaInfo.configuration.orientation = Configuration.ORIENTATION_PORTRAIT; + when(mMockAnimationController.getAnimator(any(), any(), any())).thenReturn(null); + when(mMockDisplayController.getDisplay(anyInt())).thenReturn(mDisplay); + when(mMockSurfaceTransactionHelper.translate(any(), any(), anyFloat())).thenReturn( + mMockSurfaceTransactionHelper); + when(mMockSurfaceTransactionHelper.crop(any(), any(), any())).thenReturn( + mMockSurfaceTransactionHelper); + when(mMockSurfaceTransactionHelper.round(any(), any())).thenReturn( + mMockSurfaceTransactionHelper); + when(mMockAnimator.isRunning()).thenReturn(true); + when(mMockAnimator.setDuration(anyInt())).thenReturn(mFakeAnimator); + when(mMockAnimator.setOneHandedAnimationCallbacks(any())).thenReturn(mFakeAnimator); + when(mMockAnimator.setTransitionDirection(anyInt())).thenReturn(mFakeAnimator); + when(mMockLeash.getWidth()).thenReturn(DISPLAY_WIDTH); + when(mMockLeash.getHeight()).thenReturn(DISPLAY_HEIGHT); + + mDisplayAreaOrganizer = new OneHandedDisplayAreaOrganizer(mContext, + mMockDisplayController, + mMockAnimationController, + mTutorialHandler); + mSpyUpdateHandler = spy(new Handler(OneHandedThread.get().getLooper(), mUpdateCallback)); + mDisplayAreaOrganizer.setUpdateHandler(mSpyUpdateHandler); + } + + @Test + public void testOnDisplayAreaAppeared() { + mDisplayAreaOrganizer.onDisplayAreaAppeared(mDisplayAreaInfo, mLeash); + mTestableLooper.processAllMessages(); + + verify(mMockAnimationController, never()).getAnimator(any(), any(), any()); + } + + @Test + public void testOnDisplayAreaVanished() { + mDisplayAreaOrganizer.onDisplayAreaAppeared(mDisplayAreaInfo, mLeash); + mTestableLooper.processAllMessages(); + mDisplayAreaOrganizer.onDisplayAreaVanished(mDisplayAreaInfo); + + assertThat(mDisplayAreaOrganizer.mDisplayAreaMap).isEmpty(); + } + + @Test + public void testScheduleOffset() { + final int xOffSet = 0; + final int yOffSet = 100; + mDisplayAreaOrganizer.scheduleOffset(xOffSet, yOffSet); + mTestableLooper.processAllMessages(); + + verify(mSpyUpdateHandler).sendMessage(any()); + } + + @Test + public void testRotation_portrait_0_to_landscape_90() { + when(mMockLeash.isValid()).thenReturn(false); + // Rotate 0 -> 90 + mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_0, Surface.ROTATION_90, + mMockWindowContainerTransaction); + mTestableLooper.processAllMessages(); + + verify(mSpyUpdateHandler).sendMessage(any()); + } + + @Test + public void testRotation_portrait_0_to_seascape_270() { + when(mMockLeash.isValid()).thenReturn(false); + // Rotate 0 -> 270 + mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_0, Surface.ROTATION_270, + mMockWindowContainerTransaction); + mTestableLooper.processAllMessages(); + + verify(mSpyUpdateHandler).sendMessage(any()); + } + + @Test + public void testRotation_portrait_180_to_landscape_90() { + when(mMockLeash.isValid()).thenReturn(false); + // Rotate 180 -> 90 + mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_180, Surface.ROTATION_90, + mMockWindowContainerTransaction); + mTestableLooper.processAllMessages(); + + verify(mSpyUpdateHandler).sendMessage(any()); + } + + @Test + public void testRotation_portrait_180_to_seascape_270() { + when(mMockLeash.isValid()).thenReturn(false); + // Rotate 180 -> 270 + mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_180, Surface.ROTATION_270, + mMockWindowContainerTransaction); + mTestableLooper.processAllMessages(); + + verify(mSpyUpdateHandler).sendMessage(any()); + } + + @Test + public void testRotation_landscape_90_to_portrait_0() { + when(mMockLeash.isValid()).thenReturn(false); + // Rotate 90 -> 0 + mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_90, Surface.ROTATION_0, + mMockWindowContainerTransaction); + mTestableLooper.processAllMessages(); + + verify(mSpyUpdateHandler).sendMessage(any()); + } + + @Test + public void testRotation_landscape_90_to_portrait_180() { + when(mMockLeash.isValid()).thenReturn(false); + // Rotate 90 -> 180 + mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_90, Surface.ROTATION_180, + mMockWindowContainerTransaction); + mTestableLooper.processAllMessages(); + + verify(mSpyUpdateHandler).sendMessage(any()); + } + + @Test + public void testRotation_Seascape_270_to_portrait_0() { + when(mMockLeash.isValid()).thenReturn(false); + // Rotate 270 -> 0 + mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_270, Surface.ROTATION_0, + mMockWindowContainerTransaction); + mTestableLooper.processAllMessages(); + + verify(mSpyUpdateHandler).sendMessage(any()); + } + + @Test + public void testRotation_seascape_90_to_portrait_180() { + when(mMockLeash.isValid()).thenReturn(false); + // Rotate 270 -> 180 + mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_270, Surface.ROTATION_180, + mMockWindowContainerTransaction); + mTestableLooper.processAllMessages(); + + verify(mSpyUpdateHandler).sendMessage(any()); + } + + @Test + public void testRotation_portrait_0_to_portrait_0() { + when(mMockLeash.isValid()).thenReturn(false); + // Rotate 0 -> 0 + mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_0, Surface.ROTATION_0, + mMockWindowContainerTransaction); + mTestableLooper.processAllMessages(); + + verify(mSpyUpdateHandler, never()).sendMessage(any()); + } + + @Test + public void testRotation_portrait_0_to_portrait_180() { + when(mMockLeash.isValid()).thenReturn(false); + // Rotate 0 -> 180 + mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_0, Surface.ROTATION_180, + mMockWindowContainerTransaction); + mTestableLooper.processAllMessages(); + + verify(mSpyUpdateHandler, never()).sendMessage(any()); + } + + @Test + public void testRotation_portrait_180_to_portrait_180() { + when(mMockLeash.isValid()).thenReturn(false); + // Rotate 180 -> 180 + mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_180, Surface.ROTATION_180, + mMockWindowContainerTransaction); + mTestableLooper.processAllMessages(); + + verify(mSpyUpdateHandler, never()).sendMessage(any()); + } + + @Test + public void testRotation_portrait_180_to_portrait_0() { + when(mMockLeash.isValid()).thenReturn(false); + // Rotate 180 -> 0 + mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_180, Surface.ROTATION_0, + mMockWindowContainerTransaction); + mTestableLooper.processAllMessages(); + + verify(mSpyUpdateHandler, never()).sendMessage(any()); + } + + @Test + public void testRotation_landscape_90_to_landscape_90() { + when(mMockLeash.isValid()).thenReturn(false); + // Rotate 90 -> 90 + mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_90, Surface.ROTATION_90, + mMockWindowContainerTransaction); + mTestableLooper.processAllMessages(); + + verify(mSpyUpdateHandler, never()).sendMessage(any()); + } + + @Test + public void testRotation_landscape_90_to_seascape_270() { + when(mMockLeash.isValid()).thenReturn(false); + // Rotate 90 -> 270 + mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_90, Surface.ROTATION_270, + mMockWindowContainerTransaction); + mTestableLooper.processAllMessages(); + + verify(mSpyUpdateHandler, never()).sendMessage(any()); + } + + @Test + public void testRotation_seascape_270_to_seascape_270() { + when(mMockLeash.isValid()).thenReturn(false); + // Rotate 270 -> 270 + mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_270, Surface.ROTATION_270, + mMockWindowContainerTransaction); + mTestableLooper.processAllMessages(); + + verify(mSpyUpdateHandler, never()).sendMessage(any()); + } + + @Test + public void testRotation_seascape_90_to_landscape_90() { + when(mMockLeash.isValid()).thenReturn(false); + // Rotate 270 -> 90 + mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_270, Surface.ROTATION_90, + mMockWindowContainerTransaction); + mTestableLooper.processAllMessages(); + + verify(mSpyUpdateHandler, never()).sendMessage(any()); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedEventsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedEventsTest.java new file mode 100644 index 000000000000..492c34e10ed5 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedEventsTest.java @@ -0,0 +1,105 @@ +/* + * 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.onehanded; + +import static org.junit.Assert.assertEquals; + +import androidx.test.filters.SmallTest; + +import com.android.internal.logging.UiEventLogger; +import com.android.internal.logging.testing.UiEventLoggerFake; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.Collection; + +@RunWith(Parameterized.class) +@SmallTest +public class OneHandedEventsTest extends OneHandedTestCase { + + private UiEventLoggerFake mUiEventLogger; + + @Parameterized.Parameter + public int mTag; + + @Parameterized.Parameter(1) + public String mExpectedMessage; + + public UiEventLogger.UiEventEnum mUiEvent; + + @Before + public void setFakeLoggers() { + mUiEventLogger = new UiEventLoggerFake(); + OneHandedEvents.sUiEventLogger = mUiEventLogger; + } + + @Test + public void testLogEvent() { + if (mUiEvent != null) { + assertEquals(1, mUiEventLogger.numLogs()); + assertEquals(mUiEvent.getId(), mUiEventLogger.eventId(0)); + } + } + + @Parameterized.Parameters(name = "{index}: {2}") + public static Collection<Object[]> data() { + return Arrays.asList(new Object[][]{ + // Triggers + {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_GESTURE_IN, + "writeEvent one_handed_trigger_gesture_in"}, + {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_GESTURE_OUT, + "writeEvent one_handed_trigger_gesture_out"}, + {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_OVERSPACE_OUT, + "writeEvent one_handed_trigger_overspace_out"}, + {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_POP_IME_OUT, + "writeEvent one_handed_trigger_pop_ime_out"}, + {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT, + "writeEvent one_handed_trigger_rotation_out"}, + {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_APP_TAPS_OUT, + "writeEvent one_handed_trigger_app_taps_out"}, + {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_TIMEOUT_OUT, + "writeEvent one_handed_trigger_timeout_out"}, + {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_SCREEN_OFF_OUT, + "writeEvent one_handed_trigger_screen_off_out"}, + // Settings toggles + {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_ENABLED_ON, + "writeEvent one_handed_settings_enabled_on"}, + {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_ENABLED_OFF, + "writeEvent one_handed_settings_enabled_off"}, + {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_ON, + "writeEvent one_handed_settings_app_taps_exit_on"}, + {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_OFF, + "writeEvent one_handed_settings_app_taps_exit_off"}, + {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_ON, + "writeEvent one_handed_settings_timeout_exit_on"}, + {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_OFF, + "writeEvent one_handed_settings_timeout_exit_off"}, + {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_NEVER, + "writeEvent one_handed_settings_timeout_seconds_never"}, + {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_4, + "writeEvent one_handed_settings_timeout_seconds_4"}, + {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_8, + "writeEvent one_handed_settings_timeout_seconds_8"}, + {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_12, + "writeEvent one_handed_settings_timeout_seconds_12"} + }); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedGestureHandlerTest.java new file mode 100644 index 000000000000..fb417c8ca5e8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedGestureHandlerTest.java @@ -0,0 +1,93 @@ +/* + * 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.onehanded; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.DisplayController; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class OneHandedGestureHandlerTest extends OneHandedTestCase { + OneHandedTutorialHandler mTutorialHandler; + OneHandedGestureHandler mGestureHandler; + @Mock + DisplayController mMockDisplayController; + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mTutorialHandler = new OneHandedTutorialHandler(mContext); + mGestureHandler = new OneHandedGestureHandler(mContext, mMockDisplayController); + } + + @Test + public void testSetGestureEventListener() { + OneHandedGestureHandler.OneHandedGestureEventCallback callback = + new OneHandedGestureHandler.OneHandedGestureEventCallback() { + @Override + public void onStart() {} + + @Override + public void onStop() {} + }; + + mGestureHandler.setGestureEventListener(callback); + assertThat(mGestureHandler.mGestureEventCallback).isEqualTo(callback); + } + + @Ignore("b/167943723, refactor it and fix it") + @Test + public void testReceiveNewConfig_whenThreeButtonModeEnabled() { + mGestureHandler.onOneHandedEnabled(true); + mGestureHandler.onThreeButtonModeEnabled(true); + + assertThat(mGestureHandler.mInputMonitor).isNotNull(); + assertThat(mGestureHandler.mInputEventReceiver).isNotNull(); + } + + @Test + public void testOneHandedDisabled_shouldDisposeInputChannel() { + mGestureHandler.onOneHandedEnabled(false); + + assertThat(mGestureHandler.mInputMonitor).isNull(); + assertThat(mGestureHandler.mInputEventReceiver).isNull(); + } + + @Test + public void testChangeNavBarToNon3Button_shouldDisposeInputChannel() { + mGestureHandler.onOneHandedEnabled(true); + mGestureHandler.onThreeButtonModeEnabled(false); + + assertThat(mGestureHandler.mInputMonitor).isNull(); + assertThat(mGestureHandler.mInputEventReceiver).isNull(); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedSettingsUtilTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedSettingsUtilTest.java new file mode 100644 index 000000000000..7c11138a47aa --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedSettingsUtilTest.java @@ -0,0 +1,111 @@ +/* + * 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.onehanded; + +import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_LONG_IN_SECONDS; +import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS; +import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER; +import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ContentResolver; +import android.database.ContentObserver; +import android.net.Uri; +import android.provider.Settings; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class OneHandedSettingsUtilTest extends OneHandedTestCase { + ContentResolver mContentResolver; + ContentObserver mContentObserver; + boolean mOnChanged; + + @Before + public void setUp() { + mContentResolver = mContext.getContentResolver(); + mContentObserver = new ContentObserver(mContext.getMainThreadHandler()) { + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + mOnChanged = true; + } + }; + } + + @Test + public void testRegisterSecureKeyObserver() { + final Uri result = OneHandedSettingsUtil.registerSettingsKeyObserver( + Settings.Secure.TAPS_APP_TO_EXIT, mContentResolver, mContentObserver); + + assertThat(result).isNotNull(); + + OneHandedSettingsUtil.registerSettingsKeyObserver( + Settings.Secure.TAPS_APP_TO_EXIT, mContentResolver, mContentObserver); + } + + @Test + public void testUnregisterSecureKeyObserver() { + OneHandedSettingsUtil.registerSettingsKeyObserver( + Settings.Secure.TAPS_APP_TO_EXIT, mContentResolver, mContentObserver); + OneHandedSettingsUtil.unregisterSettingsKeyObserver(mContentResolver, mContentObserver); + + assertThat(mOnChanged).isFalse(); + + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.TAPS_APP_TO_EXIT, 0); + + assertThat(mOnChanged).isFalse(); + } + + @Test + public void testGetSettingsIsOneHandedModeEnabled() { + assertThat(OneHandedSettingsUtil.getSettingsOneHandedModeEnabled( + mContentResolver)).isAnyOf(true, false); + } + + @Test + public void testGetSettingsTapsAppToExit() { + assertThat(OneHandedSettingsUtil.getSettingsTapsAppToExit( + mContentResolver)).isAnyOf(true, false); + } + + @Test + public void testGetSettingsOneHandedModeTimeout() { + assertThat(OneHandedSettingsUtil.getSettingsOneHandedModeTimeout( + mContentResolver)).isAnyOf( + ONE_HANDED_TIMEOUT_NEVER, + ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS, + ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS, + ONE_HANDED_TIMEOUT_LONG_IN_SECONDS); + } + + @Test + public void testGetSettingsSwipeToNotificationEnabled() { + assertThat(OneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled( + mContentResolver)).isAnyOf(true, false); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java new file mode 100644 index 000000000000..c7ae2a09ad67 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java @@ -0,0 +1,104 @@ +/* + * 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.onehanded; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.wm.shell.onehanded.OneHandedController.SUPPORT_ONE_HANDED_MODE; +import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS; + +import static org.junit.Assume.assumeTrue; + +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.os.SystemProperties; +import android.provider.Settings; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.After; +import org.junit.Before; + +/** + * Base class that does One Handed specific setup. + */ +public abstract class OneHandedTestCase { + static boolean sOrigEnabled; + static boolean sOrigTapsAppToExitEnabled; + static int sOrigTimeout; + static boolean sOrigSwipeToNotification; + + protected Context mContext; + + @Before + public void setupSettings() { + final Context testContext = + InstrumentationRegistry.getInstrumentation().getTargetContext(); + final DisplayManager dm = testContext.getSystemService(DisplayManager.class); + mContext = testContext.createDisplayContext(dm.getDisplay(DEFAULT_DISPLAY)); + + InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(); + + sOrigEnabled = OneHandedSettingsUtil.getSettingsOneHandedModeEnabled( + getContext().getContentResolver()); + sOrigTimeout = OneHandedSettingsUtil.getSettingsOneHandedModeTimeout( + getContext().getContentResolver()); + sOrigTapsAppToExitEnabled = OneHandedSettingsUtil.getSettingsTapsAppToExit( + getContext().getContentResolver()); + sOrigSwipeToNotification = OneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled( + getContext().getContentResolver()); + Settings.Secure.putInt(getContext().getContentResolver(), + Settings.Secure.ONE_HANDED_MODE_ENABLED, 1); + Settings.Secure.putInt(getContext().getContentResolver(), + Settings.Secure.ONE_HANDED_MODE_TIMEOUT, ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS); + Settings.Secure.putInt(getContext().getContentResolver(), + Settings.Secure.TAPS_APP_TO_EXIT, 1); + Settings.Secure.putInt(getContext().getContentResolver(), + Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED, 1); + } + + @Before + public void assumeOneHandedModeSupported() { + assumeTrue(SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false)); + } + + @After + public void restoreSettings() { + Settings.Secure.putInt(getContext().getContentResolver(), + Settings.Secure.ONE_HANDED_MODE_ENABLED, sOrigEnabled ? 1 : 0); + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.ONE_HANDED_MODE_TIMEOUT, sOrigTimeout); + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.TAPS_APP_TO_EXIT, sOrigTapsAppToExitEnabled ? 1 : 0); + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED, + sOrigSwipeToNotification ? 1 : 0); + + InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .dropShellPermissionIdentity(); + } + + protected Context getContext() { + return mContext; + } +} + diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandlerTest.java new file mode 100644 index 000000000000..e2b70c3bcc70 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandlerTest.java @@ -0,0 +1,105 @@ +/* + * 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.onehanded; + +import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_LONG_IN_SECONDS; +import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS; +import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER; +import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS; +import static com.android.wm.shell.onehanded.OneHandedTimeoutHandler.ONE_HANDED_TIMEOUT_STOP_MSG; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.verify; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class OneHandedTimeoutHandlerTest extends OneHandedTestCase { + OneHandedTimeoutHandler mTimeoutHandler; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mTimeoutHandler = Mockito.spy(OneHandedTimeoutHandler.get()); + } + + @Test + public void testTimeoutHandler_isNotNull() { + assertThat(OneHandedTimeoutHandler.get()).isNotNull(); + } + + @Test + public void testTimeoutHandler_getTimeout_defaultMedium() { + assertThat(OneHandedTimeoutHandler.get().getTimeout()).isEqualTo( + ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS); + } + + @Test + public void testTimeoutHandler_setNewTime_resetTimer() { + mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS); + verify(mTimeoutHandler).resetTimer(); + assertThat(mTimeoutHandler.sHandler.hasMessages(ONE_HANDED_TIMEOUT_STOP_MSG)).isNotNull(); + } + + @Test + public void testSetTimeoutNever_neverResetTimer() { + mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_NEVER); + assertThat(!mTimeoutHandler.sHandler.hasMessages(ONE_HANDED_TIMEOUT_STOP_MSG)).isNotNull(); + } + + @Test + public void testSetTimeoutShort() { + mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS); + verify(mTimeoutHandler).resetTimer(); + assertThat(mTimeoutHandler.sHandler.hasMessages(ONE_HANDED_TIMEOUT_STOP_MSG)).isNotNull(); + } + + @Test + public void testSetTimeoutMedium() { + mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS); + verify(mTimeoutHandler).resetTimer(); + assertThat(mTimeoutHandler.sHandler.hasMessages( + ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS)).isNotNull(); + } + + @Test + public void testSetTimeoutLong() { + mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_LONG_IN_SECONDS); + assertThat(mTimeoutHandler.getTimeout()).isEqualTo(ONE_HANDED_TIMEOUT_LONG_IN_SECONDS); + } + + @Test + public void testDragging_shouldRemoveAndSendEmptyMessageDelay() { + final boolean isDragging = true; + mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_LONG_IN_SECONDS); + mTimeoutHandler.resetTimer(); + TestableLooper.get(this).processAllMessages(); + assertThat(mTimeoutHandler.sHandler.hasMessages(ONE_HANDED_TIMEOUT_STOP_MSG)).isNotNull(); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTouchHandlerTest.java new file mode 100644 index 000000000000..c69e385b2602 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTouchHandlerTest.java @@ -0,0 +1,69 @@ +/* + * 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.onehanded; + +import static com.google.common.truth.Truth.assertThat; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class OneHandedTouchHandlerTest extends OneHandedTestCase { + OneHandedTouchHandler mTouchHandler; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mTouchHandler = new OneHandedTouchHandler(); + } + + @Test + public void testRegisterTouchEventListener() { + OneHandedTouchHandler.OneHandedTouchEventCallback callback = () -> { + }; + mTouchHandler.registerTouchEventListener(callback); + + assertThat(mTouchHandler.mTouchEventCallback).isEqualTo(callback); + } + + @Test + public void testOneHandedDisabled_shouldDisposeInputChannel() { + mTouchHandler.onOneHandedEnabled(false); + + assertThat(mTouchHandler.mInputMonitor).isNull(); + assertThat(mTouchHandler.mInputEventReceiver).isNull(); + } + + @Ignore("b/167943723, refactor it and fix it") + @Test + public void testOneHandedEnabled_monitorInputChannel() { + mTouchHandler.onOneHandedEnabled(true); + + assertThat(mTouchHandler.mInputMonitor).isNotNull(); + assertThat(mTouchHandler.mInputEventReceiver).isNotNull(); + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTutorialHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTutorialHandlerTest.java new file mode 100644 index 000000000000..3341c9cbacb9 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTutorialHandlerTest.java @@ -0,0 +1,70 @@ +/* + * 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.onehanded; + +import static org.mockito.Mockito.verify; + +import android.content.om.IOverlayManager; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.DisplayController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class OneHandedTutorialHandlerTest extends OneHandedTestCase { + @Mock + OneHandedTouchHandler mTouchHandler; + OneHandedTutorialHandler mTutorialHandler; + OneHandedGestureHandler mGestureHandler; + OneHandedController mOneHandedController; + @Mock + DisplayController mMockDisplayController; + @Mock + OneHandedDisplayAreaOrganizer mMockDisplayAreaOrganizer; + @Mock + IOverlayManager mMockOverlayManager; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mTutorialHandler = new OneHandedTutorialHandler(mContext); + mGestureHandler = new OneHandedGestureHandler(mContext, mMockDisplayController); + mOneHandedController = new OneHandedController( + getContext(), + mMockDisplayController, + mMockDisplayAreaOrganizer, + mTouchHandler, + mTutorialHandler, + mGestureHandler, + mMockOverlayManager); + } + + @Test + public void testRegisterForDisplayAreaOrganizer() { + verify(mMockDisplayAreaOrganizer).registerTransitionCallback(mTutorialHandler); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java new file mode 100644 index 000000000000..255e74917ca0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java @@ -0,0 +1,196 @@ +/* + * 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.pip; + +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import android.graphics.Matrix; +import android.graphics.Rect; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.SurfaceControl; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.pip.PipAnimationController; +import com.android.wm.shell.pip.PipSurfaceTransactionHelper; +import com.android.wm.shell.pip.PipTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Unit tests against {@link PipAnimationController} to ensure that it sends the right callbacks + * depending on the various interactions. + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class PipAnimationControllerTest extends PipTestCase { + + private PipAnimationController mPipAnimationController; + + private SurfaceControl mLeash; + + @Mock + private PipAnimationController.PipAnimationCallback mPipAnimationCallback; + + @Before + public void setUp() throws Exception { + mPipAnimationController = new PipAnimationController( + new PipSurfaceTransactionHelper(mContext)); + mLeash = new SurfaceControl.Builder() + .setContainerLayer() + .setName("FakeLeash") + .build(); + MockitoAnnotations.initMocks(this); + } + + @Test + public void getAnimator_withAlpha_returnFloatAnimator() { + final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController + .getAnimator(mLeash, new Rect(), 0f, 1f); + + assertEquals("Expect ANIM_TYPE_ALPHA animation", + animator.getAnimationType(), PipAnimationController.ANIM_TYPE_ALPHA); + } + + @Test + public void getAnimator_withBounds_returnBoundsAnimator() { + final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController + .getAnimator(mLeash, new Rect(), new Rect(), null, TRANSITION_DIRECTION_TO_PIP); + + assertEquals("Expect ANIM_TYPE_BOUNDS animation", + animator.getAnimationType(), PipAnimationController.ANIM_TYPE_BOUNDS); + } + + @Test + public void getAnimator_whenSameTypeRunning_updateExistingAnimator() { + final Rect startValue = new Rect(0, 0, 100, 100); + final Rect endValue1 = new Rect(100, 100, 200, 200); + final Rect endValue2 = new Rect(200, 200, 300, 300); + final PipAnimationController.PipTransitionAnimator oldAnimator = mPipAnimationController + .getAnimator(mLeash, startValue, endValue1, null, TRANSITION_DIRECTION_TO_PIP); + oldAnimator.setSurfaceControlTransactionFactory(DummySurfaceControlTx::new); + oldAnimator.start(); + + final PipAnimationController.PipTransitionAnimator newAnimator = mPipAnimationController + .getAnimator(mLeash, startValue, endValue2, null, TRANSITION_DIRECTION_TO_PIP); + + assertEquals("getAnimator with same type returns same animator", + oldAnimator, newAnimator); + assertEquals("getAnimator with same type updates end value", + endValue2, newAnimator.getEndValue()); + } + + @Test + public void getAnimator_setTransitionDirection() { + PipAnimationController.PipTransitionAnimator animator = mPipAnimationController + .getAnimator(mLeash, new Rect(), 0f, 1f) + .setTransitionDirection(TRANSITION_DIRECTION_TO_PIP); + assertEquals("Transition to PiP mode", + animator.getTransitionDirection(), TRANSITION_DIRECTION_TO_PIP); + + animator = mPipAnimationController + .getAnimator(mLeash, new Rect(), 0f, 1f) + .setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP); + assertEquals("Transition to fullscreen mode", + animator.getTransitionDirection(), TRANSITION_DIRECTION_LEAVE_PIP); + } + + @Test + @SuppressWarnings("unchecked") + public void pipTransitionAnimator_updateEndValue() { + final Rect startValue = new Rect(0, 0, 100, 100); + final Rect endValue1 = new Rect(100, 100, 200, 200); + final Rect endValue2 = new Rect(200, 200, 300, 300); + final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController + .getAnimator(mLeash, startValue, endValue1, null, TRANSITION_DIRECTION_TO_PIP); + + animator.updateEndValue(endValue2); + + assertEquals("updateEndValue updates end value", animator.getEndValue(), endValue2); + } + + @Test + public void pipTransitionAnimator_setPipAnimationCallback() { + final Rect startValue = new Rect(0, 0, 100, 100); + final Rect endValue = new Rect(100, 100, 200, 200); + final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController + .getAnimator(mLeash, startValue, endValue, null, TRANSITION_DIRECTION_TO_PIP); + animator.setSurfaceControlTransactionFactory(DummySurfaceControlTx::new); + + animator.setPipAnimationCallback(mPipAnimationCallback); + + // onAnimationStart triggers onPipAnimationStart + animator.onAnimationStart(animator); + verify(mPipAnimationCallback).onPipAnimationStart(animator); + + // onAnimationCancel triggers onPipAnimationCancel + animator.onAnimationCancel(animator); + verify(mPipAnimationCallback).onPipAnimationCancel(animator); + + // onAnimationEnd triggers onPipAnimationEnd + animator.onAnimationEnd(animator); + verify(mPipAnimationCallback).onPipAnimationEnd(any(SurfaceControl.Transaction.class), + eq(animator)); + } + + /** + * A dummy {@link SurfaceControl.Transaction} class. + * This is created as {@link Mock} does not support method chaining. + */ + public static class DummySurfaceControlTx extends SurfaceControl.Transaction { + @Override + public SurfaceControl.Transaction setAlpha(SurfaceControl leash, float alpha) { + return this; + } + + @Override + public SurfaceControl.Transaction setPosition(SurfaceControl leash, float x, float y) { + return this; + } + + @Override + public SurfaceControl.Transaction setWindowCrop(SurfaceControl leash, int w, int h) { + return this; + } + + @Override + public SurfaceControl.Transaction setCornerRadius(SurfaceControl leash, float radius) { + return this; + } + + @Override + public SurfaceControl.Transaction setMatrix(SurfaceControl leash, Matrix matrix, + float[] float9) { + return this; + } + + @Override + public void apply() {} + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsHandlerTest.java new file mode 100644 index 000000000000..39117bb5912b --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsHandlerTest.java @@ -0,0 +1,309 @@ +/* + * 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.pip; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.graphics.Rect; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.testing.TestableResources; +import android.util.Size; +import android.view.DisplayInfo; +import android.view.Gravity; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.pip.PipBoundsHandler; +import com.android.wm.shell.pip.PipTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Unit tests against {@link PipBoundsHandler}, including but not limited to: + * - default/movement bounds + * - save/restore PiP position on application lifecycle + * - save/restore PiP position on screen rotation + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class PipBoundsHandlerTest extends PipTestCase { + private static final int ROUNDING_ERROR_MARGIN = 16; + private static final float ASPECT_RATIO_ERROR_MARGIN = 0.01f; + private static final float DEFAULT_ASPECT_RATIO = 1f; + private static final float MIN_ASPECT_RATIO = 0.5f; + private static final float MAX_ASPECT_RATIO = 2f; + private static final Rect EMPTY_CURRENT_BOUNDS = null; + private static final Size EMPTY_MINIMAL_SIZE = null; + + private PipBoundsHandler mPipBoundsHandler; + private DisplayInfo mDefaultDisplayInfo; + private PipBoundsState mPipBoundsState; + + @Before + public void setUp() throws Exception { + initializeMockResources(); + mPipBoundsState = new PipBoundsState(); + mPipBoundsHandler = new PipBoundsHandler(mContext, mPipBoundsState); + + mPipBoundsHandler.onDisplayInfoChanged(mDefaultDisplayInfo); + } + + private void initializeMockResources() { + final TestableResources res = mContext.getOrCreateTestableResources(); + res.addOverride( + com.android.internal.R.dimen.config_pictureInPictureDefaultAspectRatio, + DEFAULT_ASPECT_RATIO); + res.addOverride( + com.android.internal.R.integer.config_defaultPictureInPictureGravity, + Gravity.END | Gravity.BOTTOM); + res.addOverride( + com.android.internal.R.dimen.default_minimal_size_pip_resizable_task, 100); + res.addOverride( + com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets, + "16x16"); + res.addOverride( + com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio, + MIN_ASPECT_RATIO); + res.addOverride( + com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio, + MAX_ASPECT_RATIO); + + mDefaultDisplayInfo = new DisplayInfo(); + mDefaultDisplayInfo.displayId = 1; + mDefaultDisplayInfo.logicalWidth = 1000; + mDefaultDisplayInfo.logicalHeight = 1500; + } + + @Test + public void getDefaultAspectRatio() { + assertEquals("Default aspect ratio matches resources", + DEFAULT_ASPECT_RATIO, mPipBoundsHandler.getDefaultAspectRatio(), + ASPECT_RATIO_ERROR_MARGIN); + } + + @Test + public void onConfigurationChanged_reloadResources() { + final float newDefaultAspectRatio = (DEFAULT_ASPECT_RATIO + MAX_ASPECT_RATIO) / 2; + final TestableResources res = mContext.getOrCreateTestableResources(); + res.addOverride(com.android.internal.R.dimen.config_pictureInPictureDefaultAspectRatio, + newDefaultAspectRatio); + + mPipBoundsHandler.onConfigurationChanged(mContext); + + assertEquals("Default aspect ratio should be reloaded", + mPipBoundsHandler.getDefaultAspectRatio(), newDefaultAspectRatio, + ASPECT_RATIO_ERROR_MARGIN); + } + + @Test + public void getDestinationBounds_returnBoundsMatchesAspectRatio() { + final float[] aspectRatios = new float[] { + (MIN_ASPECT_RATIO + DEFAULT_ASPECT_RATIO) / 2, + DEFAULT_ASPECT_RATIO, + (MAX_ASPECT_RATIO + DEFAULT_ASPECT_RATIO) / 2 + }; + for (float aspectRatio : aspectRatios) { + mPipBoundsState.setAspectRatio(aspectRatio); + final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds( + EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE); + final float actualAspectRatio = + destinationBounds.width() / (destinationBounds.height() * 1f); + assertEquals("Destination bounds matches the given aspect ratio", + aspectRatio, actualAspectRatio, ASPECT_RATIO_ERROR_MARGIN); + } + } + + @Test + public void getDestinationBounds_invalidAspectRatio_returnsDefaultAspectRatio() { + final float[] invalidAspectRatios = new float[] { + MIN_ASPECT_RATIO / 2, + MAX_ASPECT_RATIO * 2 + }; + for (float aspectRatio : invalidAspectRatios) { + mPipBoundsState.setAspectRatio(aspectRatio); + final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds( + EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE); + final float actualAspectRatio = + destinationBounds.width() / (destinationBounds.height() * 1f); + assertEquals("Destination bounds fallbacks to default aspect ratio", + mPipBoundsHandler.getDefaultAspectRatio(), actualAspectRatio, + ASPECT_RATIO_ERROR_MARGIN); + } + } + + @Test + public void getDestinationBounds_withCurrentBounds_returnBoundsMatchesAspectRatio() { + final float aspectRatio = (DEFAULT_ASPECT_RATIO + MAX_ASPECT_RATIO) / 2; + final Rect currentBounds = new Rect(0, 0, 0, 100); + currentBounds.right = (int) (currentBounds.height() * aspectRatio) + currentBounds.left; + + mPipBoundsState.setAspectRatio(aspectRatio); + final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds(currentBounds, + EMPTY_MINIMAL_SIZE); + + final float actualAspectRatio = + destinationBounds.width() / (destinationBounds.height() * 1f); + assertEquals("Destination bounds matches the given aspect ratio", + aspectRatio, actualAspectRatio, ASPECT_RATIO_ERROR_MARGIN); + } + + @Test + public void getDestinationBounds_withMinSize_returnMinBounds() { + final float[] aspectRatios = new float[] { + (MIN_ASPECT_RATIO + DEFAULT_ASPECT_RATIO) / 2, + DEFAULT_ASPECT_RATIO, + (MAX_ASPECT_RATIO + DEFAULT_ASPECT_RATIO) / 2 + }; + final Size[] minimalSizes = new Size[] { + new Size((int) (100 * aspectRatios[0]), 100), + new Size((int) (100 * aspectRatios[1]), 100), + new Size((int) (100 * aspectRatios[2]), 100) + }; + for (int i = 0; i < aspectRatios.length; i++) { + final float aspectRatio = aspectRatios[i]; + final Size minimalSize = minimalSizes[i]; + mPipBoundsState.setAspectRatio(aspectRatio); + final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds( + EMPTY_CURRENT_BOUNDS, minimalSize); + assertTrue("Destination bounds is no smaller than minimal requirement", + (destinationBounds.width() == minimalSize.getWidth() + && destinationBounds.height() >= minimalSize.getHeight()) + || (destinationBounds.height() == minimalSize.getHeight() + && destinationBounds.width() >= minimalSize.getWidth())); + final float actualAspectRatio = + destinationBounds.width() / (destinationBounds.height() * 1f); + assertEquals("Destination bounds matches the given aspect ratio", + aspectRatio, actualAspectRatio, ASPECT_RATIO_ERROR_MARGIN); + } + } + + @Test + public void getDestinationBounds_withCurrentBounds_ignoreMinBounds() { + final float aspectRatio = (DEFAULT_ASPECT_RATIO + MAX_ASPECT_RATIO) / 2; + final Rect currentBounds = new Rect(0, 0, 0, 100); + currentBounds.right = (int) (currentBounds.height() * aspectRatio) + currentBounds.left; + final Size minSize = new Size(currentBounds.width() / 2, currentBounds.height() / 2); + + mPipBoundsState.setAspectRatio(aspectRatio); + final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds( + currentBounds, minSize); + + assertTrue("Destination bounds ignores minimal size", + destinationBounds.width() > minSize.getWidth() + && destinationBounds.height() > minSize.getHeight()); + } + + @Test + public void getDestinationBounds_reentryStateExists_restoreLastSize() { + mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO); + final Rect reentryBounds = mPipBoundsHandler.getDestinationBounds( + EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE); + reentryBounds.scale(1.25f); + final float reentrySnapFraction = mPipBoundsHandler.getSnapFraction(reentryBounds); + + mPipBoundsState.saveReentryState(reentryBounds, reentrySnapFraction); + final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds( + EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE); + + assertEquals(reentryBounds.width(), destinationBounds.width()); + assertEquals(reentryBounds.height(), destinationBounds.height()); + } + + @Test + public void getDestinationBounds_reentryStateExists_restoreLastPosition() { + mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO); + final Rect reentryBounds = mPipBoundsHandler.getDestinationBounds( + EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE); + reentryBounds.offset(0, -100); + final float reentrySnapFraction = mPipBoundsHandler.getSnapFraction(reentryBounds); + + mPipBoundsState.saveReentryState(reentryBounds, reentrySnapFraction); + + final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds( + EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE); + + assertBoundsInclusionWithMargin("restoreLastPosition", reentryBounds, destinationBounds); + } + + @Test + public void setShelfHeight_offsetBounds() { + final int shelfHeight = 100; + mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO); + final Rect oldPosition = mPipBoundsHandler.getDestinationBounds( + EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE); + + mPipBoundsHandler.setShelfHeight(true, shelfHeight); + final Rect newPosition = mPipBoundsHandler.getDestinationBounds( + EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE); + + oldPosition.offset(0, -shelfHeight); + assertBoundsInclusionWithMargin("offsetBounds by shelf", oldPosition, newPosition); + } + + @Test + public void onImeVisibilityChanged_offsetBounds() { + final int imeHeight = 100; + mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO); + final Rect oldPosition = mPipBoundsHandler.getDestinationBounds( + EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE); + + mPipBoundsHandler.onImeVisibilityChanged(true, imeHeight); + final Rect newPosition = mPipBoundsHandler.getDestinationBounds( + EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE); + + oldPosition.offset(0, -imeHeight); + assertBoundsInclusionWithMargin("offsetBounds by IME", oldPosition, newPosition); + } + + @Test + public void getDestinationBounds_noReentryState_useDefaultBounds() { + mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO); + final Rect defaultBounds = mPipBoundsHandler.getDestinationBounds( + EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE); + + mPipBoundsState.clearReentryState(); + + final Rect actualBounds = mPipBoundsHandler.getDestinationBounds( + EMPTY_CURRENT_BOUNDS, EMPTY_MINIMAL_SIZE); + + assertBoundsInclusionWithMargin("useDefaultBounds", defaultBounds, actualBounds); + } + + private void assertBoundsInclusionWithMargin(String from, Rect expected, Rect actual) { + final Rect expectedWithMargin = new Rect(expected); + expectedWithMargin.inset(-ROUNDING_ERROR_MARGIN, -ROUNDING_ERROR_MARGIN); + assertTrue(from + ": expect " + expected + + " contains " + actual + + " with error margin " + ROUNDING_ERROR_MARGIN, + expectedWithMargin.contains(actual)); + } + + private void assertNonBoundsInclusionWithMargin(String from, Rect expected, Rect actual) { + final Rect expectedWithMargin = new Rect(expected); + expectedWithMargin.inset(-ROUNDING_ERROR_MARGIN, -ROUNDING_ERROR_MARGIN); + assertFalse(from + ": expect " + expected + + " not contains " + actual + + " with error margin " + ROUNDING_ERROR_MARGIN, + expectedWithMargin.contains(actual)); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java new file mode 100644 index 000000000000..dc9399edaa3b --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java @@ -0,0 +1,110 @@ +/* + * 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.pip; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import android.content.ComponentName; +import android.graphics.Rect; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.util.Size; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for {@link PipBoundsState}. + */ +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +@SmallTest +public class PipBoundsStateTest extends PipTestCase { + + private static final Rect DEFAULT_BOUNDS = new Rect(0, 0, 10, 10); + private static final float DEFAULT_SNAP_FRACTION = 1.0f; + + private PipBoundsState mPipBoundsState; + private ComponentName mTestComponentName1; + private ComponentName mTestComponentName2; + + @Before + public void setUp() { + mPipBoundsState = new PipBoundsState(); + mTestComponentName1 = new ComponentName(mContext, "component1"); + mTestComponentName2 = new ComponentName(mContext, "component2"); + } + + @Test + public void testSetBounds() { + final Rect bounds = new Rect(0, 0, 100, 100); + mPipBoundsState.setBounds(bounds); + + assertEquals(bounds, mPipBoundsState.getBounds()); + } + + @Test + public void testSetReentryState() { + final Rect bounds = new Rect(0, 0, 100, 100); + final float snapFraction = 0.5f; + + mPipBoundsState.saveReentryState(bounds, snapFraction); + + final PipBoundsState.PipReentryState state = mPipBoundsState.getReentryState(); + assertEquals(new Size(100, 100), state.getSize()); + assertEquals(snapFraction, state.getSnapFraction(), 0.01); + } + + @Test + public void testClearReentryState() { + final Rect bounds = new Rect(0, 0, 100, 100); + final float snapFraction = 0.5f; + + mPipBoundsState.saveReentryState(bounds, snapFraction); + mPipBoundsState.clearReentryState(); + + assertNull(mPipBoundsState.getReentryState()); + } + + @Test + public void testSetLastPipComponentName_notChanged_doesNotClearReentryState() { + mPipBoundsState.setLastPipComponentName(mTestComponentName1); + mPipBoundsState.saveReentryState(DEFAULT_BOUNDS, DEFAULT_SNAP_FRACTION); + + mPipBoundsState.setLastPipComponentName(mTestComponentName1); + + final PipBoundsState.PipReentryState state = mPipBoundsState.getReentryState(); + assertNotNull(state); + assertEquals(new Size(DEFAULT_BOUNDS.width(), DEFAULT_BOUNDS.height()), state.getSize()); + assertEquals(DEFAULT_SNAP_FRACTION, state.getSnapFraction(), 0.01); + } + + @Test + public void testSetLastPipComponentName_changed_clearReentryState() { + mPipBoundsState.setLastPipComponentName(mTestComponentName1); + mPipBoundsState.saveReentryState(DEFAULT_BOUNDS, DEFAULT_SNAP_FRACTION); + + mPipBoundsState.setLastPipComponentName(mTestComponentName2); + + assertNull(mPipBoundsState.getReentryState()); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java new file mode 100644 index 000000000000..54543d25b401 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java @@ -0,0 +1,74 @@ +/* + * 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.pip; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.verify; + +import android.os.RemoteException; +import android.test.suitebuilder.annotation.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.splitscreen.SplitScreen; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; + +/** + * Unit tests for {@link PipTaskOrganizer} + */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class PipTaskOrganizerTest extends PipTestCase { + private PipTaskOrganizer mSpiedPipTaskOrganizer; + + @Mock private DisplayController mMockdDisplayController; + @Mock private PipBoundsHandler mMockPipBoundsHandler; + @Mock private PipSurfaceTransactionHelper mMockPipSurfaceTransactionHelper; + @Mock private PipUiEventLogger mMockPipUiEventLogger; + @Mock private Optional<SplitScreen> mMockOptionalSplitScreen; + @Mock private ShellTaskOrganizer mMockShellTaskOrganizer; + @Mock private PipBoundsState mMockPipBoundsState; + + @Before + public void setUp() throws RemoteException { + MockitoAnnotations.initMocks(this); + mSpiedPipTaskOrganizer = new PipTaskOrganizer(mContext, mMockPipBoundsState, + mMockPipBoundsHandler, mMockPipSurfaceTransactionHelper, mMockOptionalSplitScreen, + mMockdDisplayController, mMockPipUiEventLogger, mMockShellTaskOrganizer); + } + + @Test + public void instantiatePipTaskOrganizer_addsTaskListener() { + verify(mMockShellTaskOrganizer).addListenerForType(any(), anyInt()); + } + + @Test + public void instantiatePipTaskOrganizer_addsDisplayWindowListener() { + verify(mMockdDisplayController).addDisplayWindowListener(any()); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTestCase.java new file mode 100644 index 000000000000..fdebe4e4e6f5 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTestCase.java @@ -0,0 +1,53 @@ +/* + * 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.pip; + +import static android.view.Display.DEFAULT_DISPLAY; + +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.testing.TestableContext; + +import androidx.test.InstrumentationRegistry; + +import org.junit.Before; + +/** + * Base class that does One Handed specific setup. + */ +public abstract class PipTestCase { + + protected TestableContext mContext; + + @Before + public void setup() { + final Context context = + InstrumentationRegistry.getInstrumentation().getTargetContext(); + final DisplayManager dm = context.getSystemService(DisplayManager.class); + mContext = new TestableContext( + context.createDisplayContext(dm.getDisplay(DEFAULT_DISPLAY))); + + InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(); + } + + protected Context getContext() { + return mContext; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java new file mode 100644 index 000000000000..a282a48e8494 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -0,0 +1,103 @@ +/* + * 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.pip.phone; + +import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; + +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.test.suitebuilder.annotation.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import com.android.wm.shell.WindowManagerShellWrapper; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.pip.PipBoundsHandler; +import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipTaskOrganizer; +import com.android.wm.shell.pip.PipTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Unit tests for {@link PipController} + */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class PipControllerTest extends PipTestCase { + private PipController mPipController; + + @Mock private DisplayController mMockDisplayController; + @Mock private PipMenuActivityController mMockPipMenuActivityController; + @Mock private PipAppOpsListener mMockPipAppOpsListener; + @Mock private PipBoundsHandler mMockPipBoundsHandler; + @Mock private PipMediaController mMockPipMediaController; + @Mock private PipTaskOrganizer mMockPipTaskOrganizer; + @Mock private PipTouchHandler mMockPipTouchHandler; + @Mock private WindowManagerShellWrapper mMockWindowManagerShellWrapper; + @Mock private PipBoundsState mMockPipBoundsState; + + @Before + public void setUp() throws RemoteException { + MockitoAnnotations.initMocks(this); + mPipController = new PipController(mContext, mMockDisplayController, + mMockPipAppOpsListener, mMockPipBoundsHandler, mMockPipBoundsState, + mMockPipMediaController, mMockPipMenuActivityController, mMockPipTaskOrganizer, + mMockPipTouchHandler, mMockWindowManagerShellWrapper); + } + + @Test + public void instantiatePipController_registersPipTransitionCallback() { + verify(mMockPipTaskOrganizer).registerPipTransitionCallback(any()); + } + + @Test + public void instantiatePipController_addsDisplayChangingController() { + verify(mMockDisplayController).addDisplayChangingController(any()); + } + + @Test + public void instantiatePipController_addsDisplayWindowListener() { + verify(mMockDisplayController).addDisplayWindowListener(any()); + } + + @Test + public void createPip_notSupported_returnsNull() { + Context spyContext = spy(mContext); + PackageManager mockPackageManager = mock(PackageManager.class); + when(mockPackageManager.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)).thenReturn(false); + when(spyContext.getPackageManager()).thenReturn(mockPackageManager); + + assertNull(PipController.create(spyContext, mMockDisplayController, + mMockPipAppOpsListener, mMockPipBoundsHandler, mMockPipBoundsState, + mMockPipMediaController, mMockPipMenuActivityController, mMockPipTaskOrganizer, + mMockPipTouchHandler, mMockWindowManagerShellWrapper)); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java new file mode 100644 index 000000000000..3f60cc01f20b --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java @@ -0,0 +1,161 @@ +/* + * 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.pip.phone; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.graphics.Point; +import android.graphics.Rect; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.util.Size; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.R; +import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.pip.PipBoundsHandler; +import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipSnapAlgorithm; +import com.android.wm.shell.pip.PipTaskOrganizer; +import com.android.wm.shell.pip.PipTestCase; +import com.android.wm.shell.pip.PipUiEventLogger; +import com.android.wm.shell.pip.phone.PipMenuActivityController; +import com.android.wm.shell.pip.phone.PipMotionHelper; +import com.android.wm.shell.pip.phone.PipResizeGestureHandler; +import com.android.wm.shell.pip.phone.PipTouchHandler; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +/** + * Unit tests against {@link PipTouchHandler}, including but not limited to: + * - Update movement bounds based on new bounds + * - Update movement bounds based on IME/shelf + * - Update movement bounds to PipResizeHandler + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class PipTouchHandlerTest extends PipTestCase { + + private PipTouchHandler mPipTouchHandler; + + @Mock + private PipMenuActivityController mPipMenuActivityController; + + @Mock + private PipTaskOrganizer mPipTaskOrganizer; + + @Mock + private FloatingContentCoordinator mFloatingContentCoordinator; + + @Mock + private PipUiEventLogger mPipUiEventLogger; + + private PipBoundsState mPipBoundsState; + private PipBoundsHandler mPipBoundsHandler; + private PipSnapAlgorithm mPipSnapAlgorithm; + private PipMotionHelper mMotionHelper; + private PipResizeGestureHandler mPipResizeGestureHandler; + + private Rect mInsetBounds; + private Rect mMinBounds; + private Rect mCurBounds; + private boolean mFromImeAdjustment; + private boolean mFromShelfAdjustment; + private int mDisplayRotation; + private int mImeHeight; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mPipBoundsState = new PipBoundsState(); + mPipBoundsHandler = new PipBoundsHandler(mContext, mPipBoundsState); + mPipSnapAlgorithm = mPipBoundsHandler.getSnapAlgorithm(); + mPipSnapAlgorithm = new PipSnapAlgorithm(mContext); + mPipTouchHandler = new PipTouchHandler(mContext, mPipMenuActivityController, + mPipBoundsHandler, mPipBoundsState, mPipTaskOrganizer, mFloatingContentCoordinator, + mPipUiEventLogger); + mMotionHelper = Mockito.spy(mPipTouchHandler.getMotionHelper()); + mPipResizeGestureHandler = Mockito.spy(mPipTouchHandler.getPipResizeGestureHandler()); + mPipTouchHandler.setPipMotionHelper(mMotionHelper); + mPipTouchHandler.setPipResizeGestureHandler(mPipResizeGestureHandler); + + // Assume a display of 1000 x 1000 + // inset of 10 + mInsetBounds = new Rect(10, 10, 990, 990); + // minBounds of 100x100 bottom right corner + mMinBounds = new Rect(890, 890, 990, 990); + mCurBounds = new Rect(mMinBounds); + mFromImeAdjustment = false; + mFromShelfAdjustment = false; + mDisplayRotation = 0; + mImeHeight = 100; + } + + @Test + public void updateMovementBounds_minBounds() { + Rect expectedMinMovementBounds = new Rect(); + mPipSnapAlgorithm.getMovementBounds(mMinBounds, mInsetBounds, expectedMinMovementBounds, 0); + + mPipTouchHandler.onMovementBoundsChanged(mInsetBounds, mMinBounds, mCurBounds, + mFromImeAdjustment, mFromShelfAdjustment, mDisplayRotation); + + assertEquals(expectedMinMovementBounds, mPipTouchHandler.mNormalMovementBounds); + verify(mPipResizeGestureHandler, times(1)) + .updateMinSize(mMinBounds.width(), mMinBounds.height()); + } + + @Test + public void updateMovementBounds_maxBounds() { + Point displaySize = new Point(); + mContext.getDisplay().getRealSize(displaySize); + Size maxSize = mPipSnapAlgorithm.getSizeForAspectRatio(1, + mContext.getResources().getDimensionPixelSize( + R.dimen.pip_expanded_shortest_edge_size), displaySize.x, displaySize.y); + Rect maxBounds = new Rect(0, 0, maxSize.getWidth(), maxSize.getHeight()); + Rect expectedMaxMovementBounds = new Rect(); + mPipSnapAlgorithm.getMovementBounds(maxBounds, mInsetBounds, expectedMaxMovementBounds, 0); + + mPipTouchHandler.onMovementBoundsChanged(mInsetBounds, mMinBounds, mCurBounds, + mFromImeAdjustment, mFromShelfAdjustment, mDisplayRotation); + + assertEquals(expectedMaxMovementBounds, mPipTouchHandler.mExpandedMovementBounds); + verify(mPipResizeGestureHandler, times(1)) + .updateMaxSize(maxBounds.width(), maxBounds.height()); + } + + @Test + public void updateMovementBounds_withImeAdjustment_movesPip() { + mFromImeAdjustment = true; + mPipTouchHandler.onImeVisibilityChanged(true /* imeVisible */, mImeHeight); + + mPipTouchHandler.onMovementBoundsChanged(mInsetBounds, mMinBounds, mCurBounds, + mFromImeAdjustment, mFromShelfAdjustment, mDisplayRotation); + + verify(mMotionHelper, times(1)).animateToOffset(any(), anyInt()); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchStateTest.java new file mode 100644 index 000000000000..40667f76b17e --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchStateTest.java @@ -0,0 +1,161 @@ +/* + * 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. + */ + +package com.android.wm.shell.pip.phone; + +import static android.view.MotionEvent.ACTION_BUTTON_PRESS; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_UP; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.testing.TestableLooper.RunWithLooper; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.pip.PipTestCase; +import com.android.wm.shell.pip.phone.PipTouchState; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.CountDownLatch; + +@RunWith(AndroidTestingRunner.class) +@SmallTest +@RunWithLooper +public class PipTouchStateTest extends PipTestCase { + + private PipTouchState mTouchState; + private CountDownLatch mDoubleTapCallbackTriggeredLatch; + private CountDownLatch mHoverExitCallbackTriggeredLatch; + + @Before + public void setUp() throws Exception { + mDoubleTapCallbackTriggeredLatch = new CountDownLatch(1); + mHoverExitCallbackTriggeredLatch = new CountDownLatch(1); + mTouchState = new PipTouchState(ViewConfiguration.get(getContext()), + Handler.createAsync(Looper.myLooper()), () -> { + mDoubleTapCallbackTriggeredLatch.countDown(); + }, () -> { + mHoverExitCallbackTriggeredLatch.countDown(); + }); + assertFalse(mTouchState.isDoubleTap()); + assertFalse(mTouchState.isWaitingForDoubleTap()); + } + + @Test + public void testDoubleTapLongSingleTap_notDoubleTapAndNotWaiting() { + final long currentTime = SystemClock.uptimeMillis(); + + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, + currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT + 10, 0, 0)); + assertFalse(mTouchState.isDoubleTap()); + assertFalse(mTouchState.isWaitingForDoubleTap()); + assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1); + } + + @Test + public void testDoubleTapTimeout_timeoutCallbackCalled() throws Exception { + final long currentTime = SystemClock.uptimeMillis(); + + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, + currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 10, 0, 0)); + assertFalse(mTouchState.isDoubleTap()); + assertTrue(mTouchState.isWaitingForDoubleTap()); + + assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == 10); + mTouchState.scheduleDoubleTapTimeoutCallback(); + + // TODO: Remove this sleep. Its only being added because it speeds up this test a bit. + Thread.sleep(15); + TestableLooper.get(this).processAllMessages(); + assertTrue(mDoubleTapCallbackTriggeredLatch.getCount() == 0); + } + + @Test + public void testDoubleTapDrag_doubleTapCanceled() { + final long currentTime = SystemClock.uptimeMillis(); + + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_MOVE, currentTime + 10, 500, 500)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, currentTime + 20, 500, 500)); + assertTrue(mTouchState.isDragging()); + assertFalse(mTouchState.isDoubleTap()); + assertFalse(mTouchState.isWaitingForDoubleTap()); + assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1); + } + + @Test + public void testDoubleTap_doubleTapRegistered() { + final long currentTime = SystemClock.uptimeMillis(); + + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, currentTime + 10, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, + currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 20, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, + currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 10, 0, 0)); + assertTrue(mTouchState.isDoubleTap()); + assertFalse(mTouchState.isWaitingForDoubleTap()); + assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1); + } + + @Test + public void testHoverExitTimeout_timeoutCallbackCalled() throws Exception { + mTouchState.scheduleHoverExitTimeoutCallback(); + + // TODO: Remove this sleep. Its only being added because it speeds up this test a bit. + Thread.sleep(50); + TestableLooper.get(this).processAllMessages(); + assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 0); + } + + @Test + public void testHoverExitTimeout_timeoutCallbackNotCalled() throws Exception { + mTouchState.scheduleHoverExitTimeoutCallback(); + TestableLooper.get(this).processAllMessages(); + assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 1); + } + + @Test + public void testHoverExitTimeout_timeoutCallbackNotCalled_ifButtonPress() throws Exception { + mTouchState.scheduleHoverExitTimeoutCallback(); + mTouchState.onTouchEvent(createMotionEvent(ACTION_BUTTON_PRESS, SystemClock.uptimeMillis(), + 0, 0)); + + // TODO: Remove this sleep. Its only being added because it speeds up this test a bit. + Thread.sleep(50); + TestableLooper.get(this).processAllMessages(); + assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 1); + } + + private MotionEvent createMotionEvent(int action, long eventTime, float x, float y) { + return MotionEvent.obtain(0, eventTime, action, x, y, 0); + } +} diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp index aa34edf487fe..903ca2aa0783 100644 --- a/libs/androidfw/Android.bp +++ b/libs/androidfw/Android.bp @@ -155,10 +155,12 @@ cc_test { android: { srcs: [ "tests/BackupData_test.cpp", + "tests/BackupHelpers_test.cpp", + "tests/CursorWindow_test.cpp", "tests/ObbFile_test.cpp", "tests/PosixUtils_test.cpp", ], - shared_libs: common_test_libs + ["libui"], + shared_libs: common_test_libs + ["libbinder", "liblog", "libui"], }, host: { static_libs: common_test_libs + ["liblog", "libz"], @@ -184,9 +186,28 @@ cc_benchmark { // Actual benchmarks. "tests/AssetManager2_bench.cpp", "tests/AttributeResolution_bench.cpp", + "tests/CursorWindow_bench.cpp", "tests/SparseEntry_bench.cpp", "tests/Theme_bench.cpp", ], shared_libs: common_test_libs, data: ["tests/data/**/*.apk"], } + +cc_library { + name: "libandroidfw_fuzzer_lib", + defaults: ["libandroidfw_defaults"], + host_supported: true, + srcs: [ + "CursorWindow.cpp", + ], + export_include_dirs: ["include"], + target: { + android: { + shared_libs: common_test_libs + ["libbinder", "liblog"], + }, + host: { + static_libs: common_test_libs + ["libbinder", "liblog"], + }, + }, +} diff --git a/libs/androidfw/BackupHelpers.cpp b/libs/androidfw/BackupHelpers.cpp index 8bfe2b6a259a..e80e9486c8b2 100644 --- a/libs/androidfw/BackupHelpers.cpp +++ b/libs/androidfw/BackupHelpers.cpp @@ -479,7 +479,7 @@ void send_tarfile_chunk(BackupDataWriter* writer, const char* buffer, size_t siz } int write_tarfile(const String8& packageName, const String8& domain, - const String8& rootpath, const String8& filepath, off_t* outSize, + const String8& rootpath, const String8& filepath, off64_t* outSize, BackupDataWriter* writer) { // In the output stream everything is stored relative to the root diff --git a/libs/androidfw/CursorWindow.cpp b/libs/androidfw/CursorWindow.cpp index 6f05cbd0ebb3..1b8db46c54b6 100644 --- a/libs/androidfw/CursorWindow.cpp +++ b/libs/androidfw/CursorWindow.cpp @@ -14,166 +14,275 @@ * limitations under the License. */ -#undef LOG_TAG #define LOG_TAG "CursorWindow" #include <androidfw/CursorWindow.h> -#include <binder/Parcel.h> -#include <utils/Log.h> -#include <cutils/ashmem.h> #include <sys/mman.h> -#include <assert.h> -#include <string.h> -#include <stdlib.h> +#include "android-base/logging.h" +#include "cutils/ashmem.h" namespace android { -CursorWindow::CursorWindow(const String8& name, int ashmemFd, - void* data, size_t size, bool readOnly) : - mName(name), mAshmemFd(ashmemFd), mData(data), mSize(size), mReadOnly(readOnly) { - mHeader = static_cast<Header*>(mData); +/** + * By default windows are lightweight inline allocations of this size; + * they're only inflated to ashmem regions when more space is needed. + */ +static constexpr const size_t kInlineSize = 16384; + +static constexpr const size_t kSlotShift = 4; +static constexpr const size_t kSlotSizeBytes = 1 << kSlotShift; + +CursorWindow::CursorWindow() { } CursorWindow::~CursorWindow() { - ::munmap(mData, mSize); - ::close(mAshmemFd); + if (mAshmemFd != -1) { + ::munmap(mData, mSize); + ::close(mAshmemFd); + } else { + free(mData); + } } -status_t CursorWindow::create(const String8& name, size_t size, CursorWindow** outCursorWindow) { +status_t CursorWindow::create(const String8 &name, size_t inflatedSize, CursorWindow **outWindow) { + *outWindow = nullptr; + + CursorWindow* window = new CursorWindow(); + if (!window) goto fail; + + window->mName = name; + window->mSize = std::min(kInlineSize, inflatedSize); + window->mInflatedSize = inflatedSize; + window->mData = malloc(window->mSize); + if (!window->mData) goto fail; + window->mReadOnly = false; + + window->clear(); + window->updateSlotsData(); + + LOG(DEBUG) << "Created: " << window->toString(); + *outWindow = window; + return OK; + +fail: + LOG(ERROR) << "Failed create"; +fail_silent: + delete window; + return UNKNOWN_ERROR; +} + +status_t CursorWindow::maybeInflate() { + int ashmemFd = 0; + void* newData = nullptr; + + // Bail early when we can't expand any further + if (mReadOnly || mSize == mInflatedSize) { + return INVALID_OPERATION; + } + String8 ashmemName("CursorWindow: "); - ashmemName.append(name); + ashmemName.append(mName); - status_t result; - int ashmemFd = ashmem_create_region(ashmemName.string(), size); + ashmemFd = ashmem_create_region(ashmemName.string(), mInflatedSize); if (ashmemFd < 0) { - result = -errno; - ALOGE("CursorWindow: ashmem_create_region() failed: errno=%d.", errno); - } else { - result = ashmem_set_prot_region(ashmemFd, PROT_READ | PROT_WRITE); - if (result < 0) { - ALOGE("CursorWindow: ashmem_set_prot_region() failed: errno=%d",errno); - } else { - void* data = ::mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, ashmemFd, 0); - if (data == MAP_FAILED) { - result = -errno; - ALOGE("CursorWindow: mmap() failed: errno=%d.", errno); - } else { - result = ashmem_set_prot_region(ashmemFd, PROT_READ); - if (result < 0) { - ALOGE("CursorWindow: ashmem_set_prot_region() failed: errno=%d.", errno); - } else { - CursorWindow* window = new CursorWindow(name, ashmemFd, - data, size, false /*readOnly*/); - result = window->clear(); - if (!result) { - LOG_WINDOW("Created new CursorWindow: freeOffset=%d, " - "numRows=%d, numColumns=%d, mSize=%zu, mData=%p", - window->mHeader->freeOffset, - window->mHeader->numRows, - window->mHeader->numColumns, - window->mSize, window->mData); - *outCursorWindow = window; - return OK; - } - delete window; - } - } - ::munmap(data, size); - } - ::close(ashmemFd); + PLOG(ERROR) << "Failed ashmem_create_region"; + goto fail_silent; + } + + if (ashmem_set_prot_region(ashmemFd, PROT_READ | PROT_WRITE) < 0) { + PLOG(ERROR) << "Failed ashmem_set_prot_region"; + goto fail_silent; } - *outCursorWindow = NULL; - return result; + + newData = ::mmap(nullptr, mInflatedSize, PROT_READ | PROT_WRITE, MAP_SHARED, ashmemFd, 0); + if (newData == MAP_FAILED) { + PLOG(ERROR) << "Failed mmap"; + goto fail_silent; + } + + if (ashmem_set_prot_region(ashmemFd, PROT_READ) < 0) { + PLOG(ERROR) << "Failed ashmem_set_prot_region"; + goto fail_silent; + } + + { + // Migrate existing contents into new ashmem region + uint32_t slotsSize = mSize - mSlotsOffset; + uint32_t newSlotsOffset = mInflatedSize - slotsSize; + memcpy(static_cast<uint8_t*>(newData), + static_cast<uint8_t*>(mData), mAllocOffset); + memcpy(static_cast<uint8_t*>(newData) + newSlotsOffset, + static_cast<uint8_t*>(mData) + mSlotsOffset, slotsSize); + + free(mData); + mAshmemFd = ashmemFd; + mData = newData; + mSize = mInflatedSize; + mSlotsOffset = newSlotsOffset; + + updateSlotsData(); + } + + LOG(DEBUG) << "Inflated: " << this->toString(); + return OK; + +fail: + LOG(ERROR) << "Failed maybeInflate"; +fail_silent: + ::munmap(newData, mInflatedSize); + ::close(ashmemFd); + return UNKNOWN_ERROR; } -status_t CursorWindow::createFromParcel(Parcel* parcel, CursorWindow** outCursorWindow) { - String8 name = parcel->readString8(); +status_t CursorWindow::createFromParcel(Parcel* parcel, CursorWindow** outWindow) { + *outWindow = nullptr; + + CursorWindow* window = new CursorWindow(); + if (!window) goto fail; + + if (parcel->readString8(&window->mName)) goto fail; + if (parcel->readUint32(&window->mNumRows)) goto fail; + if (parcel->readUint32(&window->mNumColumns)) goto fail; + if (parcel->readUint32(&window->mSize)) goto fail; - status_t result; - int actualSize; - int ashmemFd = parcel->readFileDescriptor(); - if (ashmemFd == int(BAD_TYPE)) { - result = BAD_TYPE; - ALOGE("CursorWindow: readFileDescriptor() failed"); + if ((window->mNumRows * window->mNumColumns * kSlotSizeBytes) > window->mSize) { + LOG(ERROR) << "Unexpected size " << window->mSize << " for " << window->mNumRows + << " rows and " << window->mNumColumns << " columns"; + goto fail_silent; + } + + bool isAshmem; + if (parcel->readBool(&isAshmem)) goto fail; + if (isAshmem) { + window->mAshmemFd = parcel->readFileDescriptor(); + if (window->mAshmemFd < 0) { + LOG(ERROR) << "Failed readFileDescriptor"; + goto fail_silent; + } + + window->mAshmemFd = ::fcntl(window->mAshmemFd, F_DUPFD_CLOEXEC, 0); + if (window->mAshmemFd < 0) { + PLOG(ERROR) << "Failed F_DUPFD_CLOEXEC"; + goto fail_silent; + } + + window->mData = ::mmap(nullptr, window->mSize, PROT_READ, MAP_SHARED, window->mAshmemFd, 0); + if (window->mData == MAP_FAILED) { + PLOG(ERROR) << "Failed mmap"; + goto fail_silent; + } } else { - ssize_t size = ashmem_get_size_region(ashmemFd); - if (size < 0) { - result = UNKNOWN_ERROR; - ALOGE("CursorWindow: ashmem_get_size_region() failed: errno=%d.", errno); - } else { - int dupAshmemFd = ::fcntl(ashmemFd, F_DUPFD_CLOEXEC, 0); - if (dupAshmemFd < 0) { - result = -errno; - ALOGE("CursorWindow: fcntl() failed: errno=%d.", errno); - } else { - // the size of the ashmem descriptor can be modified between ashmem_get_size_region - // call and mmap, so we'll check again immediately after memory is mapped - void* data = ::mmap(NULL, size, PROT_READ, MAP_SHARED, dupAshmemFd, 0); - if (data == MAP_FAILED) { - result = -errno; - ALOGE("CursorWindow: mmap() failed: errno=%d.", errno); - } else if ((actualSize = ashmem_get_size_region(dupAshmemFd)) != size) { - ::munmap(data, size); - result = BAD_VALUE; - ALOGE("CursorWindow: ashmem_get_size_region() returned %d, expected %d" - " errno=%d", - actualSize, (int) size, errno); - } else { - CursorWindow* window = new CursorWindow(name, dupAshmemFd, - data, size, true /*readOnly*/); - LOG_WINDOW("Created CursorWindow from parcel: freeOffset=%d, " - "numRows=%d, numColumns=%d, mSize=%zu, mData=%p", - window->mHeader->freeOffset, - window->mHeader->numRows, - window->mHeader->numColumns, - window->mSize, window->mData); - *outCursorWindow = window; - return OK; - } - ::close(dupAshmemFd); - } + window->mAshmemFd = -1; + + if (window->mSize > kInlineSize) { + LOG(ERROR) << "Unexpected size " << window->mSize << " for inline window"; + goto fail_silent; } + + window->mData = malloc(window->mSize); + if (!window->mData) goto fail; + + if (parcel->read(window->mData, window->mSize)) goto fail; } - *outCursorWindow = NULL; - return result; + + // We just came from a remote source, so we're read-only + // and we can't inflate ourselves + window->mInflatedSize = window->mSize; + window->mReadOnly = true; + + window->updateSlotsData(); + + LOG(DEBUG) << "Created from parcel: " << window->toString(); + *outWindow = window; + return OK; + +fail: + LOG(ERROR) << "Failed createFromParcel"; +fail_silent: + delete window; + return UNKNOWN_ERROR; } status_t CursorWindow::writeToParcel(Parcel* parcel) { - status_t status = parcel->writeString8(mName); - if (!status) { - status = parcel->writeDupFileDescriptor(mAshmemFd); + LOG(DEBUG) << "Writing to parcel: " << this->toString(); + + if (parcel->writeString8(mName)) goto fail; + if (parcel->writeUint32(mNumRows)) goto fail; + if (parcel->writeUint32(mNumColumns)) goto fail; + if (mAshmemFd != -1) { + if (parcel->writeUint32(mSize)) goto fail; + if (parcel->writeBool(true)) goto fail; + if (parcel->writeDupFileDescriptor(mAshmemFd)) goto fail; + } else { + // Since we know we're going to be read-only on the remote side, + // we can compact ourselves on the wire, with just enough padding + // to ensure our slots stay aligned + size_t slotsSize = mSize - mSlotsOffset; + size_t compactedSize = mAllocOffset + slotsSize; + compactedSize = (compactedSize + 3) & ~3; + if (parcel->writeUint32(compactedSize)) goto fail; + if (parcel->writeBool(false)) goto fail; + void* dest = parcel->writeInplace(compactedSize); + if (!dest) goto fail; + memcpy(static_cast<uint8_t*>(dest), + static_cast<uint8_t*>(mData), mAllocOffset); + memcpy(static_cast<uint8_t*>(dest) + compactedSize - slotsSize, + static_cast<uint8_t*>(mData) + mSlotsOffset, slotsSize); } - return status; + return OK; + +fail: + LOG(ERROR) << "Failed writeToParcel"; +fail_silent: + return UNKNOWN_ERROR; } status_t CursorWindow::clear() { if (mReadOnly) { return INVALID_OPERATION; } + mAllocOffset = 0; + mSlotsOffset = mSize; + mNumRows = 0; + mNumColumns = 0; + return OK; +} - mHeader->freeOffset = sizeof(Header) + sizeof(RowSlotChunk); - mHeader->firstChunkOffset = sizeof(Header); - mHeader->numRows = 0; - mHeader->numColumns = 0; +void CursorWindow::updateSlotsData() { + mSlotsStart = static_cast<uint8_t*>(mData) + mSize - kSlotSizeBytes; + mSlotsEnd = static_cast<uint8_t*>(mData) + mSlotsOffset; +} - RowSlotChunk* firstChunk = static_cast<RowSlotChunk*>(offsetToPtr(mHeader->firstChunkOffset)); - firstChunk->nextChunkOffset = 0; - return OK; +void* CursorWindow::offsetToPtr(uint32_t offset, uint32_t bufferSize = 0) { + if (offset > mSize) { + LOG(ERROR) << "Offset " << offset + << " out of bounds, max value " << mSize; + return nullptr; + } + if (offset + bufferSize > mSize) { + LOG(ERROR) << "End offset " << (offset + bufferSize) + << " out of bounds, max value " << mSize; + return nullptr; + } + return static_cast<uint8_t*>(mData) + offset; +} + +uint32_t CursorWindow::offsetFromPtr(void* ptr) { + return static_cast<uint8_t*>(ptr) - static_cast<uint8_t*>(mData); } status_t CursorWindow::setNumColumns(uint32_t numColumns) { if (mReadOnly) { return INVALID_OPERATION; } - - uint32_t cur = mHeader->numColumns; - if ((cur > 0 || mHeader->numRows > 0) && cur != numColumns) { - ALOGE("Trying to go from %d columns to %d", cur, numColumns); + uint32_t cur = mNumColumns; + if ((cur > 0 || mNumRows > 0) && cur != numColumns) { + LOG(ERROR) << "Trying to go from " << cur << " columns to " << numColumns; return INVALID_OPERATION; } - mHeader->numColumns = numColumns; + mNumColumns = numColumns; return OK; } @@ -181,28 +290,19 @@ status_t CursorWindow::allocRow() { if (mReadOnly) { return INVALID_OPERATION; } - - // Fill in the row slot - RowSlot* rowSlot = allocRowSlot(); - if (rowSlot == NULL) { - return NO_MEMORY; - } - - // Allocate the slots for the field directory - size_t fieldDirSize = mHeader->numColumns * sizeof(FieldSlot); - uint32_t fieldDirOffset = alloc(fieldDirSize, true /*aligned*/); - if (!fieldDirOffset) { - mHeader->numRows--; - LOG_WINDOW("The row failed, so back out the new row accounting " - "from allocRowSlot %d", mHeader->numRows); - return NO_MEMORY; + size_t size = mNumColumns * kSlotSizeBytes; + int32_t newOffset = mSlotsOffset - size; + if (newOffset < (int32_t) mAllocOffset) { + maybeInflate(); + newOffset = mSlotsOffset - size; + if (newOffset < (int32_t) mAllocOffset) { + return NO_MEMORY; + } } - FieldSlot* fieldDir = static_cast<FieldSlot*>(offsetToPtr(fieldDirOffset)); - memset(fieldDir, 0, fieldDirSize); - - LOG_WINDOW("Allocated row %u, rowSlot is at offset %u, fieldDir is %zu bytes at offset %u\n", - mHeader->numRows - 1, offsetFromPtr(rowSlot), fieldDirSize, fieldDirOffset); - rowSlot->offset = fieldDirOffset; + memset(offsetToPtr(newOffset), 0, size); + mSlotsOffset = newOffset; + updateSlotsData(); + mNumRows++; return OK; } @@ -210,83 +310,48 @@ status_t CursorWindow::freeLastRow() { if (mReadOnly) { return INVALID_OPERATION; } - - if (mHeader->numRows > 0) { - mHeader->numRows--; + size_t size = mNumColumns * kSlotSizeBytes; + size_t newOffset = mSlotsOffset + size; + if (newOffset > mSize) { + return NO_MEMORY; } + mSlotsOffset = newOffset; + updateSlotsData(); + mNumRows--; return OK; } -uint32_t CursorWindow::alloc(size_t size, bool aligned) { - uint32_t padding; - if (aligned) { - // 4 byte alignment - padding = (~mHeader->freeOffset + 1) & 3; - } else { - padding = 0; - } - - uint32_t offset = mHeader->freeOffset + padding; - uint32_t nextFreeOffset = offset + size; - if (nextFreeOffset > mSize) { - ALOGW("Window is full: requested allocation %zu bytes, " - "free space %zu bytes, window size %zu bytes", - size, freeSpace(), mSize); - return 0; - } - - mHeader->freeOffset = nextFreeOffset; - return offset; -} - -CursorWindow::RowSlot* CursorWindow::getRowSlot(uint32_t row) { - uint32_t chunkPos = row; - RowSlotChunk* chunk = static_cast<RowSlotChunk*>( - offsetToPtr(mHeader->firstChunkOffset)); - while (chunkPos >= ROW_SLOT_CHUNK_NUM_ROWS) { - chunk = static_cast<RowSlotChunk*>(offsetToPtr(chunk->nextChunkOffset)); - chunkPos -= ROW_SLOT_CHUNK_NUM_ROWS; - } - return &chunk->slots[chunkPos]; -} - -CursorWindow::RowSlot* CursorWindow::allocRowSlot() { - uint32_t chunkPos = mHeader->numRows; - RowSlotChunk* chunk = static_cast<RowSlotChunk*>( - offsetToPtr(mHeader->firstChunkOffset)); - while (chunkPos > ROW_SLOT_CHUNK_NUM_ROWS) { - chunk = static_cast<RowSlotChunk*>(offsetToPtr(chunk->nextChunkOffset)); - chunkPos -= ROW_SLOT_CHUNK_NUM_ROWS; +status_t CursorWindow::alloc(size_t size, uint32_t* outOffset) { + if (mReadOnly) { + return INVALID_OPERATION; } - if (chunkPos == ROW_SLOT_CHUNK_NUM_ROWS) { - if (!chunk->nextChunkOffset) { - chunk->nextChunkOffset = alloc(sizeof(RowSlotChunk), true /*aligned*/); - if (!chunk->nextChunkOffset) { - return NULL; - } + size_t alignedSize = (size + 3) & ~3; + size_t newOffset = mAllocOffset + alignedSize; + if (newOffset > mSlotsOffset) { + maybeInflate(); + newOffset = mAllocOffset + alignedSize; + if (newOffset > mSlotsOffset) { + return NO_MEMORY; } - chunk = static_cast<RowSlotChunk*>(offsetToPtr(chunk->nextChunkOffset)); - chunk->nextChunkOffset = 0; - chunkPos = 0; } - mHeader->numRows += 1; - return &chunk->slots[chunkPos]; + *outOffset = mAllocOffset; + mAllocOffset = newOffset; + return OK; } CursorWindow::FieldSlot* CursorWindow::getFieldSlot(uint32_t row, uint32_t column) { - if (row >= mHeader->numRows || column >= mHeader->numColumns) { - ALOGE("Failed to read row %d, column %d from a CursorWindow which " - "has %d rows, %d columns.", - row, column, mHeader->numRows, mHeader->numColumns); - return NULL; - } - RowSlot* rowSlot = getRowSlot(row); - if (!rowSlot) { - ALOGE("Failed to find rowSlot for row %d.", row); - return NULL; + // This is carefully tuned to use as few cycles as + // possible, since this is an extremely hot code path; + // see CursorWindow_bench.cpp for more details + void *result = static_cast<uint8_t*>(mSlotsStart) + - (((row * mNumColumns) + column) << kSlotShift); + if (result < mSlotsEnd || result > mSlotsStart || column >= mNumColumns) { + LOG(ERROR) << "Failed to read row " << row << ", column " << column + << " from a window with " << mNumRows << " rows, " << mNumColumns << " columns"; + return nullptr; + } else { + return static_cast<FieldSlot*>(result); } - FieldSlot* fieldDir = static_cast<FieldSlot*>(offsetToPtr(rowSlot->offset)); - return &fieldDir[column]; } status_t CursorWindow::putBlob(uint32_t row, uint32_t column, const void* value, size_t size) { @@ -309,13 +374,14 @@ status_t CursorWindow::putBlobOrString(uint32_t row, uint32_t column, return BAD_VALUE; } - uint32_t offset = alloc(size); - if (!offset) { + uint32_t offset; + if (alloc(size, &offset)) { return NO_MEMORY; } memcpy(offsetToPtr(offset), value, size); + fieldSlot = getFieldSlot(row, column); fieldSlot->type = type; fieldSlot->data.buffer.offset = offset; fieldSlot->data.buffer.size = size; diff --git a/libs/androidfw/fuzz/cursorwindow_fuzzer/Android.bp b/libs/androidfw/fuzz/cursorwindow_fuzzer/Android.bp new file mode 100644 index 000000000000..2dac47b0dac6 --- /dev/null +++ b/libs/androidfw/fuzz/cursorwindow_fuzzer/Android.bp @@ -0,0 +1,31 @@ +cc_fuzz { + name: "cursorwindow_fuzzer", + srcs: [ + "cursorwindow_fuzzer.cpp", + ], + host_supported: true, + corpus: ["corpus/*"], + static_libs: ["libgmock"], + target: { + android: { + shared_libs: [ + "libandroidfw_fuzzer_lib", + "libbase", + "libbinder", + "libcutils", + "liblog", + "libutils", + ], + }, + host: { + static_libs: [ + "libandroidfw_fuzzer_lib", + "libbase", + "libbinder", + "libcutils", + "liblog", + "libutils", + ], + }, + }, +} diff --git a/libs/androidfw/fuzz/cursorwindow_fuzzer/corpus/typical.bin b/libs/androidfw/fuzz/cursorwindow_fuzzer/corpus/typical.bin Binary files differnew file mode 100644 index 000000000000..c7e22dd26ea7 --- /dev/null +++ b/libs/androidfw/fuzz/cursorwindow_fuzzer/corpus/typical.bin diff --git a/libs/androidfw/fuzz/cursorwindow_fuzzer/cursorwindow_fuzzer.cpp b/libs/androidfw/fuzz/cursorwindow_fuzzer/cursorwindow_fuzzer.cpp new file mode 100644 index 000000000000..8dce21220199 --- /dev/null +++ b/libs/androidfw/fuzz/cursorwindow_fuzzer/cursorwindow_fuzzer.cpp @@ -0,0 +1,78 @@ +/* + * 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. + */ + +#include <stddef.h> +#include <stdint.h> +#include <string.h> +#include <string> +#include <memory> + +#include "android-base/logging.h" +#include "androidfw/CursorWindow.h" +#include "binder/Parcel.h" + +#include <fuzzer/FuzzedDataProvider.h> + +using android::CursorWindow; +using android::Parcel; + +extern "C" int LLVMFuzzerInitialize(int *, char ***) { + setenv("ANDROID_LOG_TAGS", "*:s", 1); + android::base::InitLogging(nullptr, &android::base::StderrLogger); + return 0; +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + Parcel p; + p.setData(data, size); + + CursorWindow* w = nullptr; + if (!CursorWindow::createFromParcel(&p, &w)) { + LOG(WARNING) << "Valid cursor with " << w->getNumRows() << " rows, " + << w->getNumColumns() << " cols"; + + // Try obtaining heap allocations for most items; we trim the + // search space to speed things up + auto rows = std::min(w->getNumRows(), static_cast<uint32_t>(128)); + auto cols = std::min(w->getNumColumns(), static_cast<uint32_t>(128)); + for (auto row = 0; row < rows; row++) { + for (auto col = 0; col < cols; col++) { + auto field = w->getFieldSlot(row, col); + if (!field) continue; + switch (w->getFieldSlotType(field)) { + case CursorWindow::FIELD_TYPE_STRING: { + size_t size; + w->getFieldSlotValueString(field, &size); + break; + } + case CursorWindow::FIELD_TYPE_BLOB: { + size_t size; + w->getFieldSlotValueBlob(field, &size); + break; + } + } + } + } + + // Finally, try obtaining the furthest valid field + if (rows > 0 && cols > 0) { + w->getFieldSlot(w->getNumRows() - 1, w->getNumColumns() - 1); + } + } + delete w; + + return 0; +} diff --git a/libs/androidfw/include/androidfw/BackupHelpers.h b/libs/androidfw/include/androidfw/BackupHelpers.h index 2da247b77c0a..a0fa13662cb9 100644 --- a/libs/androidfw/include/androidfw/BackupHelpers.h +++ b/libs/androidfw/include/androidfw/BackupHelpers.h @@ -137,7 +137,7 @@ int back_up_files(int oldSnapshotFD, BackupDataWriter* dataStream, int newSnapsh char const* const* files, char const* const *keys, int fileCount); int write_tarfile(const String8& packageName, const String8& domain, - const String8& rootPath, const String8& filePath, off_t* outSize, + const String8& rootPath, const String8& filePath, off64_t* outSize, BackupDataWriter* outputStream); class RestoreHelperBase diff --git a/libs/androidfw/include/androidfw/CursorWindow.h b/libs/androidfw/include/androidfw/CursorWindow.h index ad64b246b3f5..6e55a9a0eb8b 100644 --- a/libs/androidfw/include/androidfw/CursorWindow.h +++ b/libs/androidfw/include/androidfw/CursorWindow.h @@ -20,38 +20,36 @@ #include <inttypes.h> #include <stddef.h> #include <stdint.h> +#include <string> -#include <binder/Parcel.h> -#include <log/log.h> -#include <utils/String8.h> +#include "android-base/stringprintf.h" +#include "binder/Parcel.h" +#include "utils/String8.h" -#if LOG_NDEBUG - -#define IF_LOG_WINDOW() if (false) #define LOG_WINDOW(...) -#else - -#define IF_LOG_WINDOW() IF_ALOG(LOG_DEBUG, "CursorWindow") -#define LOG_WINDOW(...) ALOG(LOG_DEBUG, "CursorWindow", __VA_ARGS__) - -#endif - namespace android { /** - * This class stores a set of rows from a database in a buffer. The begining of the - * window has first chunk of RowSlots, which are offsets to the row directory, followed by - * an offset to the next chunk in a linked-list of additional chunk of RowSlots in case - * the pre-allocated chunk isn't big enough to refer to all rows. Each row directory has a - * FieldSlot per column, which has the size, offset, and type of the data for that field. - * Note that the data types come from sqlite3.h. + * This class stores a set of rows from a database in a buffer. Internally + * data is structured as a "heap" of string/blob allocations at the bottom + * of the memory region, and a "stack" of FieldSlot allocations at the top + * of the memory region. Here's an example visual representation: + * + * +----------------------------------------------------------------+ + * |heap\0of\0strings\0 222211110000| ... + * +-------------------+--------------------------------+-------+---+ + * ^ ^ ^ ^ ^ ^ + * | | | | | | + * | +- mAllocOffset mSlotsOffset -+ | | | + * +- mData mSlotsStart -+ | | + * mSize -+ | + * mInflatedSize -+ * * Strings are stored in UTF-8. */ class CursorWindow { - CursorWindow(const String8& name, int ashmemFd, - void* data, size_t size, bool readOnly); + CursorWindow(); public: /* Field types. */ @@ -88,9 +86,9 @@ public: inline String8 name() { return mName; } inline size_t size() { return mSize; } - inline size_t freeSpace() { return mSize - mHeader->freeOffset; } - inline uint32_t getNumRows() { return mHeader->numRows; } - inline uint32_t getNumColumns() { return mHeader->numColumns; } + inline size_t freeSpace() { return mSlotsOffset - mAllocOffset; } + inline uint32_t getNumRows() { return mNumRows; } + inline uint32_t getNumColumns() { return mNumColumns; } status_t clear(); status_t setNumColumns(uint32_t numColumns); @@ -138,62 +136,57 @@ public: return offsetToPtr(fieldSlot->data.buffer.offset, fieldSlot->data.buffer.size); } -private: - static const size_t ROW_SLOT_CHUNK_NUM_ROWS = 100; - - struct Header { - // Offset of the lowest unused byte in the window. - uint32_t freeOffset; - - // Offset of the first row slot chunk. - uint32_t firstChunkOffset; - - uint32_t numRows; - uint32_t numColumns; - }; - - struct RowSlot { - uint32_t offset; - }; - - struct RowSlotChunk { - RowSlot slots[ROW_SLOT_CHUNK_NUM_ROWS]; - uint32_t nextChunkOffset; - }; + inline std::string toString() const { + return android::base::StringPrintf("CursorWindow{name=%s, fd=%d, size=%d, inflatedSize=%d, " + "allocOffset=%d, slotsOffset=%d, numRows=%d, numColumns=%d}", mName.c_str(), + mAshmemFd, mSize, mInflatedSize, mAllocOffset, mSlotsOffset, mNumRows, mNumColumns); + } +private: String8 mName; - int mAshmemFd; - void* mData; - size_t mSize; - bool mReadOnly; - Header* mHeader; - - inline void* offsetToPtr(uint32_t offset, uint32_t bufferSize = 0) { - if (offset >= mSize) { - ALOGE("Offset %" PRIu32 " out of bounds, max value %zu", offset, mSize); - return NULL; - } - if (offset + bufferSize > mSize) { - ALOGE("End offset %" PRIu32 " out of bounds, max value %zu", - offset + bufferSize, mSize); - return NULL; - } - return static_cast<uint8_t*>(mData) + offset; - } + int mAshmemFd = -1; + void* mData = nullptr; + /** + * Pointer to the first FieldSlot, used to optimize the extremely + * hot code path of getFieldSlot(). + */ + void* mSlotsStart = nullptr; + void* mSlotsEnd = nullptr; + uint32_t mSize = 0; + /** + * When a window starts as lightweight inline allocation, this value + * holds the "full" size to be created after ashmem inflation. + */ + uint32_t mInflatedSize = 0; + /** + * Offset to the top of the "heap" of string/blob allocations. By + * storing these allocations at the bottom of our memory region we + * avoid having to rewrite offsets when inflating. + */ + uint32_t mAllocOffset = 0; + /** + * Offset to the bottom of the "stack" of FieldSlot allocations. + */ + uint32_t mSlotsOffset = 0; + uint32_t mNumRows = 0; + uint32_t mNumColumns = 0; + bool mReadOnly = false; - inline uint32_t offsetFromPtr(void* ptr) { - return static_cast<uint8_t*>(ptr) - static_cast<uint8_t*>(mData); - } + void updateSlotsData(); + + void* offsetToPtr(uint32_t offset, uint32_t bufferSize); + uint32_t offsetFromPtr(void* ptr); /** - * Allocate a portion of the window. Returns the offset - * of the allocation, or 0 if there isn't enough space. - * If aligned is true, the allocation gets 4 byte alignment. + * By default windows are lightweight inline allocations; this method + * inflates the window into a larger ashmem region. */ - uint32_t alloc(size_t size, bool aligned = false); + status_t maybeInflate(); - RowSlot* getRowSlot(uint32_t row); - RowSlot* allocRowSlot(); + /** + * Allocate a portion of the window. + */ + status_t alloc(size_t size, uint32_t* outOffset); status_t putBlobOrString(uint32_t row, uint32_t column, const void* value, size_t size, int32_t type); diff --git a/libs/androidfw/tests/BackupHelpers_test.cpp b/libs/androidfw/tests/BackupHelpers_test.cpp new file mode 100644 index 000000000000..86b7fb361228 --- /dev/null +++ b/libs/androidfw/tests/BackupHelpers_test.cpp @@ -0,0 +1,63 @@ +/* + * 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. + */ + +#define LOG_TAG "BackupHelpers_test" +#include <androidfw/BackupHelpers.h> + +#include <gtest/gtest.h> + +#include <fcntl.h> +#include <utils/String8.h> +#include <android-base/file.h> + +namespace android { + +class BackupHelpersTest : public testing::Test { +protected: + + virtual void SetUp() { + } + virtual void TearDown() { + } +}; + +TEST_F(BackupHelpersTest, WriteTarFileWithSizeLessThan2GB) { + TemporaryFile tf; + // Allocate a 1 KB file. + off64_t fileSize = 1024; + ASSERT_EQ(0, posix_fallocate64(tf.fd, 0, fileSize)); + off64_t tarSize = 0; + int err = write_tarfile(/* packageName */ String8("test-pkg"), /* domain */ String8(""), /* rootpath */ String8(""), /* filePath */ String8(tf.path), /* outSize */ &tarSize, /* writer */ NULL); + ASSERT_EQ(err, 0); + // Returned tarSize includes 512 B for the header. + off64_t expectedTarSize = fileSize + 512; + ASSERT_EQ(tarSize, expectedTarSize); +} + +TEST_F(BackupHelpersTest, WriteTarFileWithSizeGreaterThan2GB) { + TemporaryFile tf; + // Allocate a 2 GB file. + off64_t fileSize = 2ll * 1024ll * 1024ll * 1024ll + 512ll; + ASSERT_EQ(0, posix_fallocate64(tf.fd, 0, fileSize)); + off64_t tarSize = 0; + int err = write_tarfile(/* packageName */ String8("test-pkg"), /* domain */ String8(""), /* rootpath */ String8(""), /* filePath */ String8(tf.path), /* outSize */ &tarSize, /* writer */ NULL); + ASSERT_EQ(err, 0); + // Returned tarSize includes 512 B for the header. + off64_t expectedTarSize = fileSize + 512; + ASSERT_EQ(tarSize, expectedTarSize); +} +} + diff --git a/libs/androidfw/tests/CursorWindow_bench.cpp b/libs/androidfw/tests/CursorWindow_bench.cpp new file mode 100644 index 000000000000..f1191c3d7213 --- /dev/null +++ b/libs/androidfw/tests/CursorWindow_bench.cpp @@ -0,0 +1,86 @@ +/* + * 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. + */ + +#include "benchmark/benchmark.h" + +#include "androidfw/CursorWindow.h" + +namespace android { + +static void BM_CursorWindowWrite(benchmark::State& state, size_t rows, size_t cols) { + CursorWindow* w; + CursorWindow::create(String8("test"), 1 << 21, &w); + + while (state.KeepRunning()) { + w->clear(); + w->setNumColumns(cols); + for (int row = 0; row < rows; row++) { + w->allocRow(); + for (int col = 0; col < cols; col++) { + w->putLong(row, col, 0xcafe); + } + } + } +} + +static void BM_CursorWindowWrite4x4(benchmark::State& state) { + BM_CursorWindowWrite(state, 4, 4); +} +BENCHMARK(BM_CursorWindowWrite4x4); + +static void BM_CursorWindowWrite1Kx4(benchmark::State& state) { + BM_CursorWindowWrite(state, 1024, 4); +} +BENCHMARK(BM_CursorWindowWrite1Kx4); + +static void BM_CursorWindowWrite16Kx4(benchmark::State& state) { + BM_CursorWindowWrite(state, 16384, 4); +} +BENCHMARK(BM_CursorWindowWrite16Kx4); + +static void BM_CursorWindowRead(benchmark::State& state, size_t rows, size_t cols) { + CursorWindow* w; + CursorWindow::create(String8("test"), 1 << 21, &w); + w->setNumColumns(cols); + for (int row = 0; row < rows; row++) { + w->allocRow(); + } + + while (state.KeepRunning()) { + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + w->getFieldSlot(row, col); + } + } + } +} + +static void BM_CursorWindowRead4x4(benchmark::State& state) { + BM_CursorWindowRead(state, 4, 4); +} +BENCHMARK(BM_CursorWindowRead4x4); + +static void BM_CursorWindowRead1Kx4(benchmark::State& state) { + BM_CursorWindowRead(state, 1024, 4); +} +BENCHMARK(BM_CursorWindowRead1Kx4); + +static void BM_CursorWindowRead16Kx4(benchmark::State& state) { + BM_CursorWindowRead(state, 16384, 4); +} +BENCHMARK(BM_CursorWindowRead16Kx4); + +} // namespace android diff --git a/libs/androidfw/tests/CursorWindow_test.cpp b/libs/androidfw/tests/CursorWindow_test.cpp new file mode 100644 index 000000000000..15be80c48192 --- /dev/null +++ b/libs/androidfw/tests/CursorWindow_test.cpp @@ -0,0 +1,367 @@ +/* + * 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. + */ + +#include <utility> + +#include "androidfw/CursorWindow.h" + +#include "TestHelpers.h" + +#define CREATE_WINDOW_1K \ + CursorWindow* w; \ + CursorWindow::create(String8("test"), 1 << 10, &w); + +#define CREATE_WINDOW_1K_3X3 \ + CursorWindow* w; \ + CursorWindow::create(String8("test"), 1 << 10, &w); \ + ASSERT_EQ(w->setNumColumns(3), OK); \ + ASSERT_EQ(w->allocRow(), OK); \ + ASSERT_EQ(w->allocRow(), OK); \ + ASSERT_EQ(w->allocRow(), OK); + +#define CREATE_WINDOW_2M \ + CursorWindow* w; \ + CursorWindow::create(String8("test"), 1 << 21, &w); + +static constexpr const size_t kHalfInlineSize = 8192; +static constexpr const size_t kGiantSize = 1048576; + +namespace android { + +TEST(CursorWindowTest, Empty) { + CREATE_WINDOW_1K; + + ASSERT_EQ(w->getNumRows(), 0); + ASSERT_EQ(w->getNumColumns(), 0); + ASSERT_EQ(w->size(), 1 << 10); + ASSERT_EQ(w->freeSpace(), 1 << 10); +} + +TEST(CursorWindowTest, SetNumColumns) { + CREATE_WINDOW_1K; + + // Once we've locked in columns, we can't adjust + ASSERT_EQ(w->getNumColumns(), 0); + ASSERT_EQ(w->setNumColumns(4), OK); + ASSERT_NE(w->setNumColumns(5), OK); + ASSERT_NE(w->setNumColumns(3), OK); + ASSERT_EQ(w->getNumColumns(), 4); +} + +TEST(CursorWindowTest, SetNumColumnsAfterRow) { + CREATE_WINDOW_1K; + + // Once we've locked in a row, we can't adjust columns + ASSERT_EQ(w->getNumColumns(), 0); + ASSERT_EQ(w->allocRow(), OK); + ASSERT_NE(w->setNumColumns(4), OK); + ASSERT_EQ(w->getNumColumns(), 0); +} + +TEST(CursorWindowTest, AllocRow) { + CREATE_WINDOW_1K; + + ASSERT_EQ(w->setNumColumns(4), OK); + + // Rolling forward means we have less free space + ASSERT_EQ(w->getNumRows(), 0); + auto before = w->freeSpace(); + ASSERT_EQ(w->allocRow(), OK); + ASSERT_LT(w->freeSpace(), before); + ASSERT_EQ(w->getNumRows(), 1); + + // Verify we can unwind + ASSERT_EQ(w->freeLastRow(), OK); + ASSERT_EQ(w->freeSpace(), before); + ASSERT_EQ(w->getNumRows(), 0); + + // Can't unwind when no rows left + ASSERT_NE(w->freeLastRow(), OK); +} + +TEST(CursorWindowTest, AllocRowBounds) { + CREATE_WINDOW_1K; + + // 60 columns is 960 bytes, which means only a single row can fit + ASSERT_EQ(w->setNumColumns(60), OK); + ASSERT_EQ(w->allocRow(), OK); + ASSERT_NE(w->allocRow(), OK); +} + +TEST(CursorWindowTest, StoreNull) { + CREATE_WINDOW_1K_3X3; + + ASSERT_EQ(w->putNull(1, 1), OK); + ASSERT_EQ(w->putNull(0, 0), OK); + + { + auto field = w->getFieldSlot(1, 1); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_NULL); + } + { + auto field = w->getFieldSlot(0, 0); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_NULL); + } +} + +TEST(CursorWindowTest, StoreLong) { + CREATE_WINDOW_1K_3X3; + + ASSERT_EQ(w->putLong(1, 1, 0xf00d), OK); + ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK); + + { + auto field = w->getFieldSlot(1, 1); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER); + ASSERT_EQ(w->getFieldSlotValueLong(field), 0xf00d); + } + { + auto field = w->getFieldSlot(0, 0); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER); + ASSERT_EQ(w->getFieldSlotValueLong(field), 0xcafe); + } +} + +TEST(CursorWindowTest, StoreString) { + CREATE_WINDOW_1K_3X3; + + ASSERT_EQ(w->putString(1, 1, "food", 5), OK); + ASSERT_EQ(w->putString(0, 0, "cafe", 5), OK); + + size_t size; + { + auto field = w->getFieldSlot(1, 1); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_STRING); + auto actual = w->getFieldSlotValueString(field, &size); + ASSERT_EQ(std::string(actual), "food"); + } + { + auto field = w->getFieldSlot(0, 0); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_STRING); + auto actual = w->getFieldSlotValueString(field, &size); + ASSERT_EQ(std::string(actual), "cafe"); + } +} + +TEST(CursorWindowTest, StoreBounds) { + CREATE_WINDOW_1K_3X3; + + // Can't work with values beyond bounds + ASSERT_NE(w->putLong(0, 3, 0xcafe), OK); + ASSERT_NE(w->putLong(3, 0, 0xcafe), OK); + ASSERT_NE(w->putLong(3, 3, 0xcafe), OK); + ASSERT_EQ(w->getFieldSlot(0, 3), nullptr); + ASSERT_EQ(w->getFieldSlot(3, 0), nullptr); + ASSERT_EQ(w->getFieldSlot(3, 3), nullptr); + + // Can't work with invalid indexes + ASSERT_NE(w->putLong(-1, 0, 0xcafe), OK); + ASSERT_NE(w->putLong(0, -1, 0xcafe), OK); + ASSERT_NE(w->putLong(-1, -1, 0xcafe), OK); + ASSERT_EQ(w->getFieldSlot(-1, 0), nullptr); + ASSERT_EQ(w->getFieldSlot(0, -1), nullptr); + ASSERT_EQ(w->getFieldSlot(-1, -1), nullptr); +} + +TEST(CursorWindowTest, Inflate) { + CREATE_WINDOW_2M; + + auto before = w->size(); + ASSERT_EQ(w->setNumColumns(4), OK); + ASSERT_EQ(w->allocRow(), OK); + + // Scratch buffer that will fit before inflation + void* buf = malloc(kHalfInlineSize); + + // Store simple value + ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK); + + // Store first object that fits inside + memset(buf, 42, kHalfInlineSize); + ASSERT_EQ(w->putBlob(0, 1, buf, kHalfInlineSize), OK); + ASSERT_EQ(w->size(), before); + + // Store second simple value + ASSERT_EQ(w->putLong(0, 2, 0xface), OK); + + // Store second object that requires inflation + memset(buf, 84, kHalfInlineSize); + ASSERT_EQ(w->putBlob(0, 3, buf, kHalfInlineSize), OK); + ASSERT_GT(w->size(), before); + + // Verify data is intact + { + auto field = w->getFieldSlot(0, 0); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER); + ASSERT_EQ(w->getFieldSlotValueLong(field), 0xcafe); + } + { + auto field = w->getFieldSlot(0, 1); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB); + size_t actualSize; + auto actual = w->getFieldSlotValueBlob(field, &actualSize); + ASSERT_EQ(actualSize, kHalfInlineSize); + memset(buf, 42, kHalfInlineSize); + ASSERT_NE(actual, buf); + ASSERT_EQ(memcmp(buf, actual, kHalfInlineSize), 0); + } + { + auto field = w->getFieldSlot(0, 2); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER); + ASSERT_EQ(w->getFieldSlotValueLong(field), 0xface); + } + { + auto field = w->getFieldSlot(0, 3); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB); + size_t actualSize; + auto actual = w->getFieldSlotValueBlob(field, &actualSize); + ASSERT_EQ(actualSize, kHalfInlineSize); + memset(buf, 84, kHalfInlineSize); + ASSERT_NE(actual, buf); + ASSERT_EQ(memcmp(buf, actual, kHalfInlineSize), 0); + } +} + +TEST(CursorWindowTest, ParcelEmpty) { + CREATE_WINDOW_2M; + + Parcel p; + w->writeToParcel(&p); + p.setDataPosition(0); + w = nullptr; + + ASSERT_EQ(CursorWindow::createFromParcel(&p, &w), OK); + ASSERT_EQ(w->getNumRows(), 0); + ASSERT_EQ(w->getNumColumns(), 0); + ASSERT_EQ(w->size(), 0); + ASSERT_EQ(w->freeSpace(), 0); + + // We can't mutate the window after parceling + ASSERT_NE(w->setNumColumns(4), OK); + ASSERT_NE(w->allocRow(), OK); +} + +TEST(CursorWindowTest, ParcelSmall) { + CREATE_WINDOW_2M; + + auto before = w->size(); + ASSERT_EQ(w->setNumColumns(4), OK); + ASSERT_EQ(w->allocRow(), OK); + + // Scratch buffer that will fit before inflation + void* buf = malloc(kHalfInlineSize); + + // Store simple value + ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK); + + // Store first object that fits inside + memset(buf, 42, kHalfInlineSize); + ASSERT_EQ(w->putBlob(0, 1, buf, kHalfInlineSize), OK); + ASSERT_EQ(w->size(), before); + + // Store second object with zero length + ASSERT_EQ(w->putBlob(0, 2, buf, 0), OK); + ASSERT_EQ(w->size(), before); + + // Force through a parcel + Parcel p; + w->writeToParcel(&p); + p.setDataPosition(0); + w = nullptr; + + ASSERT_EQ(CursorWindow::createFromParcel(&p, &w), OK); + ASSERT_EQ(w->getNumRows(), 1); + ASSERT_EQ(w->getNumColumns(), 4); + + // Verify data is intact + { + auto field = w->getFieldSlot(0, 0); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER); + ASSERT_EQ(w->getFieldSlotValueLong(field), 0xcafe); + } + { + auto field = w->getFieldSlot(0, 1); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB); + size_t actualSize; + auto actual = w->getFieldSlotValueBlob(field, &actualSize); + ASSERT_EQ(actualSize, kHalfInlineSize); + memset(buf, 42, kHalfInlineSize); + ASSERT_NE(actual, buf); + ASSERT_EQ(memcmp(buf, actual, kHalfInlineSize), 0); + } + { + auto field = w->getFieldSlot(0, 2); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB); + size_t actualSize; + auto actual = w->getFieldSlotValueBlob(field, &actualSize); + ASSERT_EQ(actualSize, 0); + ASSERT_NE(actual, nullptr); + } +} + +TEST(CursorWindowTest, ParcelLarge) { + CREATE_WINDOW_2M; + + ASSERT_EQ(w->setNumColumns(4), OK); + ASSERT_EQ(w->allocRow(), OK); + + // Store simple value + ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK); + + // Store object that forces inflation + void* buf = malloc(kGiantSize); + memset(buf, 42, kGiantSize); + ASSERT_EQ(w->putBlob(0, 1, buf, kGiantSize), OK); + + // Store second object with zero length + ASSERT_EQ(w->putBlob(0, 2, buf, 0), OK); + + // Force through a parcel + Parcel p; + w->writeToParcel(&p); + p.setDataPosition(0); + w = nullptr; + + ASSERT_EQ(CursorWindow::createFromParcel(&p, &w), OK); + ASSERT_EQ(w->getNumRows(), 1); + ASSERT_EQ(w->getNumColumns(), 4); + + // Verify data is intact + { + auto field = w->getFieldSlot(0, 0); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER); + ASSERT_EQ(w->getFieldSlotValueLong(field), 0xcafe); + } + { + auto field = w->getFieldSlot(0, 1); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB); + size_t actualSize; + auto actual = w->getFieldSlotValueBlob(field, &actualSize); + ASSERT_EQ(actualSize, kGiantSize); + memset(buf, 42, kGiantSize); + ASSERT_EQ(memcmp(buf, actual, kGiantSize), 0); + } + { + auto field = w->getFieldSlot(0, 2); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB); + size_t actualSize; + auto actual = w->getFieldSlotValueBlob(field, &actualSize); + ASSERT_EQ(actualSize, 0); + ASSERT_NE(actual, nullptr); + } +} + +} // android diff --git a/libs/hostgraphics/Android.bp b/libs/hostgraphics/Android.bp index e713b98b867e..9d83e491fdc1 100644 --- a/libs/hostgraphics/Android.bp +++ b/libs/hostgraphics/Android.bp @@ -5,6 +5,10 @@ cc_library_host_static { "-Wno-unused-parameter", ], + static_libs: [ + "libbase", + ], + srcs: [ ":libui_host_common", "Fence.cpp", @@ -28,4 +32,4 @@ cc_library_host_static { enabled: true, } }, -}
\ No newline at end of file +} diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index aa842ff6a7b7..155bb6ba8f75 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -53,8 +53,6 @@ cc_defaults { host: { include_dirs: [ "external/vulkan-headers/include", - "frameworks/native/libs/math/include", - "frameworks/native/libs/ui/include", ], cflags: [ "-Wno-unused-variable", @@ -71,6 +69,10 @@ cc_defaults { "libminikin", ], + static_libs: [ + "libui-types", + ], + target: { android: { shared_libs: [ @@ -83,7 +85,6 @@ cc_defaults { "libGLESv2", "libGLESv3", "libvulkan", - "libui", "libnativedisplay", "libnativewindow", "libprotobuf-cpp-lite", @@ -152,6 +153,45 @@ cc_defaults { } // ------------------------ +// framework-graphics jar +// ------------------------ + +java_sdk_library { + name: "framework-graphics", + defaults: ["framework-module-defaults"], + visibility: [ + "//frameworks/base", // Framework + ], + + srcs: [ + ":framework-graphics-srcs", + ], + + permitted_packages: [ + "android.graphics", + ], + + // TODO: once framework-graphics is officially part of the + // UI-rendering module this line would no longer be + // needed. + installable: true, + + // Disable api_lint that the defaults enable + // TODO: enable this + api_lint: { + enabled: false, + }, +} + +filegroup { + name: "framework-graphics-srcs", + srcs: [ + "apex/java/**/*.java", + ], + path: "apex/java" +} + +// ------------------------ // APEX // ------------------------ @@ -287,6 +327,7 @@ cc_defaults { "jni/PathMeasure.cpp", "jni/Picture.cpp", "jni/Shader.cpp", + "jni/RenderEffect.cpp", "jni/Typeface.cpp", "jni/Utils.cpp", "jni/YuvToJpegEncoder.cpp", @@ -294,6 +335,7 @@ cc_defaults { "jni/fonts/FontFamily.cpp", "jni/text/LineBreaker.cpp", "jni/text/MeasuredText.cpp", + "jni/text/TextShaper.cpp", ], header_libs: [ "android_graphics_jni_headers" ], @@ -346,6 +388,7 @@ cc_defaults { "libstatspull", "libstatssocket", "libpdfium", + "libbinder_ndk", ], static_libs: [ "libgif", @@ -457,6 +500,7 @@ cc_defaults { "service/GraphicsStatsService.cpp", "thread/CommonPool.cpp", "utils/GLUtils.cpp", + "utils/NdkUtils.cpp", "utils/StringUtils.cpp", "AutoBackendTextureRelease.cpp", "DeferredLayerUpdater.cpp", @@ -499,6 +543,11 @@ cc_library { "android_graphics_jni", ], export_header_lib_headers: ["android_graphics_apex_headers"], + target: { + android: { + version_script: "libhwui.map.txt", + } + }, } cc_library_static { @@ -516,6 +565,7 @@ cc_defaults { android: { shared_libs: [ "libgui", + "libui", ], } }, diff --git a/libs/hwui/AnimationContext.h b/libs/hwui/AnimationContext.h index 74d5e79c0b77..f8a2072ffbdb 100644 --- a/libs/hwui/AnimationContext.h +++ b/libs/hwui/AnimationContext.h @@ -77,8 +77,8 @@ class AnimationContext { PREVENT_COPY_AND_ASSIGN(AnimationContext); public: - ANDROID_API explicit AnimationContext(renderthread::TimeLord& clock); - ANDROID_API virtual ~AnimationContext(); + explicit AnimationContext(renderthread::TimeLord& clock); + virtual ~AnimationContext(); nsecs_t frameTimeMs() { return mFrameTimeMs; } bool hasAnimations() { @@ -87,22 +87,22 @@ public: // Will always add to the next frame list, which is swapped when // startFrame() is called - ANDROID_API void addAnimatingRenderNode(RenderNode& node); + void addAnimatingRenderNode(RenderNode& node); // Marks the start of a frame, which will update the frame time and move all // next frame animations into the current frame - ANDROID_API virtual void startFrame(TreeInfo::TraversalMode mode); + virtual void startFrame(TreeInfo::TraversalMode mode); // Runs any animations still left in mCurrentFrameAnimations that were not run // as part of the standard RenderNode:prepareTree pass. - ANDROID_API virtual void runRemainingAnimations(TreeInfo& info); + virtual void runRemainingAnimations(TreeInfo& info); - ANDROID_API virtual void callOnFinished(BaseRenderNodeAnimator* animator, + virtual void callOnFinished(BaseRenderNodeAnimator* animator, AnimationListener* listener); - ANDROID_API virtual void destroy(); + virtual void destroy(); - ANDROID_API virtual void pauseAnimators() {} + virtual void pauseAnimators() {} private: friend class AnimationHandle; diff --git a/libs/hwui/Animator.h b/libs/hwui/Animator.h index ed7b6eb1cf4a..3c9f1ea1b6e3 100644 --- a/libs/hwui/Animator.h +++ b/libs/hwui/Animator.h @@ -39,10 +39,10 @@ class RenderProperties; class AnimationListener : public VirtualLightRefBase { public: - ANDROID_API virtual void onAnimationFinished(BaseRenderNodeAnimator*) = 0; + virtual void onAnimationFinished(BaseRenderNodeAnimator*) = 0; protected: - ANDROID_API virtual ~AnimationListener() {} + virtual ~AnimationListener() {} }; enum class RepeatMode { @@ -55,34 +55,34 @@ class BaseRenderNodeAnimator : public VirtualLightRefBase { PREVENT_COPY_AND_ASSIGN(BaseRenderNodeAnimator); public: - ANDROID_API void setStartValue(float value); - ANDROID_API void setInterpolator(Interpolator* interpolator); - ANDROID_API void setDuration(nsecs_t durationInMs); - ANDROID_API nsecs_t duration() { return mDuration; } - ANDROID_API void setStartDelay(nsecs_t startDelayInMs); - ANDROID_API nsecs_t startDelay() { return mStartDelay; } - ANDROID_API void setListener(AnimationListener* listener) { mListener = listener; } + void setStartValue(float value); + void setInterpolator(Interpolator* interpolator); + void setDuration(nsecs_t durationInMs); + nsecs_t duration() { return mDuration; } + void setStartDelay(nsecs_t startDelayInMs); + nsecs_t startDelay() { return mStartDelay; } + void setListener(AnimationListener* listener) { mListener = listener; } AnimationListener* listener() { return mListener.get(); } - ANDROID_API void setAllowRunningAsync(bool mayRunAsync) { mMayRunAsync = mayRunAsync; } + void setAllowRunningAsync(bool mayRunAsync) { mMayRunAsync = mayRunAsync; } bool mayRunAsync() { return mMayRunAsync; } - ANDROID_API void start(); - ANDROID_API virtual void reset(); - ANDROID_API void reverse(); + void start(); + virtual void reset(); + void reverse(); // Terminates the animation at its current progress. - ANDROID_API void cancel(); + void cancel(); // Terminates the animation and skip to the end of the animation. - ANDROID_API virtual void end(); + virtual void end(); void attach(RenderNode* target); virtual void onAttached() {} void detach() { mTarget = nullptr; } - ANDROID_API void pushStaging(AnimationContext& context); - ANDROID_API bool animate(AnimationContext& context); + void pushStaging(AnimationContext& context); + bool animate(AnimationContext& context); // Returns the remaining time in ms for the animation. Note this should only be called during // an animation on RenderThread. - ANDROID_API nsecs_t getRemainingPlayTime(); + nsecs_t getRemainingPlayTime(); bool isRunning() { return mPlayState == PlayState::Running || mPlayState == PlayState::Reversing; @@ -90,7 +90,7 @@ public: bool isFinished() { return mPlayState == PlayState::Finished; } float finalValue() { return mFinalValue; } - ANDROID_API virtual uint32_t dirtyMask() = 0; + virtual uint32_t dirtyMask() = 0; void forceEndNow(AnimationContext& context); RenderNode* target() { return mTarget; } @@ -196,9 +196,9 @@ public: ALPHA, }; - ANDROID_API RenderPropertyAnimator(RenderProperty property, float finalValue); + RenderPropertyAnimator(RenderProperty property, float finalValue); - ANDROID_API virtual uint32_t dirtyMask(); + virtual uint32_t dirtyMask(); protected: virtual float getValue(RenderNode* target) const override; @@ -221,10 +221,10 @@ private: class CanvasPropertyPrimitiveAnimator : public BaseRenderNodeAnimator { public: - ANDROID_API CanvasPropertyPrimitiveAnimator(CanvasPropertyPrimitive* property, + CanvasPropertyPrimitiveAnimator(CanvasPropertyPrimitive* property, float finalValue); - ANDROID_API virtual uint32_t dirtyMask(); + virtual uint32_t dirtyMask(); protected: virtual float getValue(RenderNode* target) const override; @@ -241,10 +241,10 @@ public: ALPHA, }; - ANDROID_API CanvasPropertyPaintAnimator(CanvasPropertyPaint* property, PaintField field, + CanvasPropertyPaintAnimator(CanvasPropertyPaint* property, PaintField field, float finalValue); - ANDROID_API virtual uint32_t dirtyMask(); + virtual uint32_t dirtyMask(); protected: virtual float getValue(RenderNode* target) const override; @@ -257,9 +257,9 @@ private: class RevealAnimator : public BaseRenderNodeAnimator { public: - ANDROID_API RevealAnimator(int centerX, int centerY, float startValue, float finalValue); + RevealAnimator(int centerX, int centerY, float startValue, float finalValue); - ANDROID_API virtual uint32_t dirtyMask(); + virtual uint32_t dirtyMask(); protected: virtual float getValue(RenderNode* target) const override; diff --git a/libs/hwui/AnimatorManager.h b/libs/hwui/AnimatorManager.h index 9575391a8b3f..a0df01d5962c 100644 --- a/libs/hwui/AnimatorManager.h +++ b/libs/hwui/AnimatorManager.h @@ -54,7 +54,7 @@ public: void animateNoDamage(TreeInfo& info); // Hard-ends all animators. May only be called on the UI thread. - ANDROID_API void endAllStagingAnimators(); + void endAllStagingAnimators(); // Hard-ends all animators that have been pushed. Used for cleanup if // the ActivityContext is being destroyed diff --git a/libs/hwui/AutoBackendTextureRelease.cpp b/libs/hwui/AutoBackendTextureRelease.cpp index 72747e8fa543..33264d5d5c86 100644 --- a/libs/hwui/AutoBackendTextureRelease.cpp +++ b/libs/hwui/AutoBackendTextureRelease.cpp @@ -25,7 +25,8 @@ using namespace android::uirenderer::renderthread; namespace android { namespace uirenderer { -AutoBackendTextureRelease::AutoBackendTextureRelease(GrContext* context, AHardwareBuffer* buffer) { +AutoBackendTextureRelease::AutoBackendTextureRelease(GrDirectContext* context, + AHardwareBuffer* buffer) { AHardwareBuffer_Desc desc; AHardwareBuffer_describe(buffer, &desc); bool createProtectedImage = 0 != (desc.usage & AHARDWAREBUFFER_USAGE_PROTECTED_CONTENT); @@ -67,8 +68,9 @@ static void releaseProc(SkImage::ReleaseContext releaseContext) { textureRelease->unref(false); } -void AutoBackendTextureRelease::makeImage(AHardwareBuffer* buffer, android_dataspace dataspace, - GrContext* context) { +void AutoBackendTextureRelease::makeImage(AHardwareBuffer* buffer, + android_dataspace dataspace, + GrDirectContext* context) { AHardwareBuffer_Desc desc; AHardwareBuffer_describe(buffer, &desc); SkColorType colorType = GrAHardwareBufferUtils::GetSkColorTypeFromBufferFormat(desc.format); @@ -81,7 +83,7 @@ void AutoBackendTextureRelease::makeImage(AHardwareBuffer* buffer, android_datas } } -void AutoBackendTextureRelease::newBufferContent(GrContext* context) { +void AutoBackendTextureRelease::newBufferContent(GrDirectContext* context) { if (mBackendTexture.isValid()) { mUpdateProc(mImageCtx, context); } diff --git a/libs/hwui/AutoBackendTextureRelease.h b/libs/hwui/AutoBackendTextureRelease.h index acdd63cb7921..06f51fcd1105 100644 --- a/libs/hwui/AutoBackendTextureRelease.h +++ b/libs/hwui/AutoBackendTextureRelease.h @@ -31,7 +31,8 @@ namespace uirenderer { */ class AutoBackendTextureRelease final { public: - AutoBackendTextureRelease(GrContext* context, AHardwareBuffer* buffer); + AutoBackendTextureRelease(GrDirectContext* context, + AHardwareBuffer* buffer); const GrBackendTexture& getTexture() const { return mBackendTexture; } @@ -42,9 +43,11 @@ public: inline sk_sp<SkImage> getImage() const { return mImage; } - void makeImage(AHardwareBuffer* buffer, android_dataspace dataspace, GrContext* context); + void makeImage(AHardwareBuffer* buffer, + android_dataspace dataspace, + GrDirectContext* context); - void newBufferContent(GrContext* context); + void newBufferContent(GrDirectContext* context); private: // The only way to invoke dtor is with unref, when mUsageCount is 0. diff --git a/libs/hwui/CanvasTransform.cpp b/libs/hwui/CanvasTransform.cpp index 8c37d73366c2..9d03ce5252a3 100644 --- a/libs/hwui/CanvasTransform.cpp +++ b/libs/hwui/CanvasTransform.cpp @@ -22,7 +22,6 @@ #include <SkGradientShader.h> #include <SkPaint.h> #include <SkShader.h> -#include <ui/ColorSpace.h> #include <algorithm> #include <cmath> diff --git a/libs/hwui/ColorMode.h b/libs/hwui/ColorMode.h new file mode 100644 index 000000000000..6d387f9ef43d --- /dev/null +++ b/libs/hwui/ColorMode.h @@ -0,0 +1,34 @@ +/* + * 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. + */ + +#pragma once + +namespace android::uirenderer { + +// Must match the constants in ActivityInfo.java +enum class ColorMode { + // SRGB means HWUI will produce buffer in SRGB color space. + Default = 0, + // WideColorGamut selects the most optimal colorspace & format for the device's display + // Most commonly DisplayP3 + RGBA_8888 currently. + WideColorGamut = 1, + // HDR Rec2020 + F16 + Hdr = 2, + // HDR Rec2020 + 1010102 + Hdr10 = 3, +}; + +} // namespace android::uirenderer diff --git a/libs/hwui/DamageAccumulator.h b/libs/hwui/DamageAccumulator.h index 030a20f31c42..2faa9d012d66 100644 --- a/libs/hwui/DamageAccumulator.h +++ b/libs/hwui/DamageAccumulator.h @@ -58,7 +58,7 @@ public: // Returns the current dirty area, *NOT* transformed by pushed transforms void peekAtDirty(SkRect* dest) const; - ANDROID_API void computeCurrentTransform(Matrix4* outMatrix) const; + void computeCurrentTransform(Matrix4* outMatrix) const; void finish(SkRect* totalDirty); diff --git a/libs/hwui/DeferredLayerUpdater.cpp b/libs/hwui/DeferredLayerUpdater.cpp index 67d8c07e61de..6589dbd50cf7 100644 --- a/libs/hwui/DeferredLayerUpdater.cpp +++ b/libs/hwui/DeferredLayerUpdater.cpp @@ -189,7 +189,7 @@ void DeferredLayerUpdater::detachSurfaceTexture() { sk_sp<SkImage> DeferredLayerUpdater::ImageSlot::createIfNeeded(AHardwareBuffer* buffer, android_dataspace dataspace, bool forceCreate, - GrContext* context) { + GrDirectContext* context) { if (!mTextureRelease || !mTextureRelease->getImage().get() || dataspace != mDataspace || forceCreate || mBuffer != buffer) { if (buffer != mBuffer) { diff --git a/libs/hwui/DeferredLayerUpdater.h b/libs/hwui/DeferredLayerUpdater.h index c44c0d537fa7..6731e9c428d6 100644 --- a/libs/hwui/DeferredLayerUpdater.h +++ b/libs/hwui/DeferredLayerUpdater.h @@ -44,11 +44,11 @@ class DeferredLayerUpdater : public VirtualLightRefBase, public IGpuContextCallb public: // Note that DeferredLayerUpdater assumes it is taking ownership of the layer // and will not call incrementRef on it as a result. - ANDROID_API explicit DeferredLayerUpdater(RenderState& renderState); + explicit DeferredLayerUpdater(RenderState& renderState); - ANDROID_API ~DeferredLayerUpdater(); + ~DeferredLayerUpdater(); - ANDROID_API bool setSize(int width, int height) { + bool setSize(int width, int height) { if (mWidth != width || mHeight != height) { mWidth = width; mHeight = height; @@ -60,7 +60,7 @@ public: int getWidth() { return mWidth; } int getHeight() { return mHeight; } - ANDROID_API bool setBlend(bool blend) { + bool setBlend(bool blend) { if (blend != mBlend) { mBlend = blend; return true; @@ -68,18 +68,18 @@ public: return false; } - ANDROID_API void setSurfaceTexture(AutoTextureRelease&& consumer); + void setSurfaceTexture(AutoTextureRelease&& consumer); - ANDROID_API void updateTexImage() { mUpdateTexImage = true; } + void updateTexImage() { mUpdateTexImage = true; } - ANDROID_API void setTransform(const SkMatrix* matrix) { + void setTransform(const SkMatrix* matrix) { delete mTransform; mTransform = matrix ? new SkMatrix(*matrix) : nullptr; } SkMatrix* getTransform() { return mTransform; } - ANDROID_API void setPaint(const SkPaint* paint); + void setPaint(const SkPaint* paint); void apply(); @@ -106,7 +106,7 @@ private: ~ImageSlot() { clear(); } sk_sp<SkImage> createIfNeeded(AHardwareBuffer* buffer, android_dataspace dataspace, - bool forceCreate, GrContext* context); + bool forceCreate, GrDirectContext* context); private: void clear(); diff --git a/libs/hwui/DeviceInfo.cpp b/libs/hwui/DeviceInfo.cpp index c24224cbbd67..07594715a84c 100644 --- a/libs/hwui/DeviceInfo.cpp +++ b/libs/hwui/DeviceInfo.cpp @@ -15,6 +15,8 @@ */ #include <DeviceInfo.h> +#include <android/hardware_buffer.h> +#include <apex/display.h> #include <log/log.h> #include <utils/Errors.h> @@ -30,14 +32,47 @@ DeviceInfo* DeviceInfo::get() { DeviceInfo::DeviceInfo() { #if HWUI_NULL_GPU - mMaxTextureSize = NULL_GPU_MAX_TEXTURE_SIZE; + mMaxTextureSize = NULL_GPU_MAX_TEXTURE_SIZE; #else - mMaxTextureSize = -1; + mMaxTextureSize = -1; #endif - updateDisplayInfo(); } -DeviceInfo::~DeviceInfo() { - ADisplay_release(mDisplays); + +void DeviceInfo::updateDisplayInfo() { + if (Properties::isolatedProcess) { + return; + } + + ADisplay** displays; + int size = ADisplay_acquirePhysicalDisplays(&displays); + + if (size <= 0) { + LOG_ALWAYS_FATAL("Failed to acquire physical displays for WCG support!"); + } + + for (int i = 0; i < size; ++i) { + // Pick the first internal display for querying the display type + // In practice this is controlled by a sysprop so it doesn't really + // matter which display we use. + if (ADisplay_getDisplayType(displays[i]) == DISPLAY_TYPE_INTERNAL) { + // We get the dataspace from DisplayManager already. Allocate space + // for the result here but we don't actually care about using it. + ADataSpace dataspace; + AHardwareBuffer_Format pixelFormat; + ADisplay_getPreferredWideColorFormat(displays[i], &dataspace, &pixelFormat); + + if (pixelFormat == AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM) { + mWideColorType = SkColorType::kN32_SkColorType; + } else if (pixelFormat == AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT) { + mWideColorType = SkColorType::kRGBA_F16_SkColorType; + } else { + LOG_ALWAYS_FATAL("Unreachable: unsupported pixel format: %d", pixelFormat); + } + ADisplay_release(displays); + return; + } + } + LOG_ALWAYS_FATAL("Failed to find a valid physical display for WCG support!"); } int DeviceInfo::maxTextureSize() const { @@ -49,75 +84,29 @@ void DeviceInfo::setMaxTextureSize(int maxTextureSize) { DeviceInfo::get()->mMaxTextureSize = maxTextureSize; } +void DeviceInfo::setWideColorDataspace(ADataSpace dataspace) { + switch (dataspace) { + case ADATASPACE_DISPLAY_P3: + get()->mWideColorSpace = + SkColorSpace::MakeRGB(SkNamedTransferFn::kSRGB, SkNamedGamut::kDisplayP3); + break; + case ADATASPACE_SCRGB: + get()->mWideColorSpace = SkColorSpace::MakeSRGB(); + break; + case ADATASPACE_SRGB: + // when sRGB is returned, it means wide color gamut is not supported. + get()->mWideColorSpace = SkColorSpace::MakeSRGB(); + break; + default: + LOG_ALWAYS_FATAL("Unreachable: unsupported wide color space."); + } +} + void DeviceInfo::onRefreshRateChanged(int64_t vsyncPeriod) { mVsyncPeriod = vsyncPeriod; } -void DeviceInfo::updateDisplayInfo() { - if (Properties::isolatedProcess) { - return; - } - - if (mCurrentConfig == nullptr) { - mDisplaysSize = ADisplay_acquirePhysicalDisplays(&mDisplays); - LOG_ALWAYS_FATAL_IF(mDisplays == nullptr || mDisplaysSize <= 0, - "Failed to get physical displays: no connected display: %d!", mDisplaysSize); - for (size_t i = 0; i < mDisplaysSize; i++) { - ADisplayType type = ADisplay_getDisplayType(mDisplays[i]); - if (type == ADisplayType::DISPLAY_TYPE_INTERNAL) { - mPhysicalDisplayIndex = i; - break; - } - } - LOG_ALWAYS_FATAL_IF(mPhysicalDisplayIndex < 0, "Failed to find a connected physical display!"); - - - // Since we now just got the primary display for the first time, then - // store the primary display metadata here. - ADisplay* primaryDisplay = mDisplays[mPhysicalDisplayIndex]; - mMaxRefreshRate = ADisplay_getMaxSupportedFps(primaryDisplay); - ADataSpace dataspace; - AHardwareBuffer_Format format; - ADisplay_getPreferredWideColorFormat(primaryDisplay, &dataspace, &format); - switch (dataspace) { - case ADATASPACE_DISPLAY_P3: - mWideColorSpace = - SkColorSpace::MakeRGB(SkNamedTransferFn::kSRGB, SkNamedGamut::kDCIP3); - break; - case ADATASPACE_SCRGB: - mWideColorSpace = SkColorSpace::MakeSRGB(); - break; - case ADATASPACE_SRGB: - // when sRGB is returned, it means wide color gamut is not supported. - mWideColorSpace = SkColorSpace::MakeSRGB(); - break; - default: - LOG_ALWAYS_FATAL("Unreachable: unsupported wide color space."); - } - switch (format) { - case AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM: - mWideColorType = SkColorType::kN32_SkColorType; - break; - case AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT: - mWideColorType = SkColorType::kRGBA_F16_SkColorType; - break; - default: - LOG_ALWAYS_FATAL("Unreachable: unsupported pixel format."); - } - } - // This method may have been called when the display config changed, so - // sync with the current configuration. - ADisplay* primaryDisplay = mDisplays[mPhysicalDisplayIndex]; - status_t status = ADisplay_getCurrentConfig(primaryDisplay, &mCurrentConfig); - LOG_ALWAYS_FATAL_IF(status, "Failed to get display config, error %d", status); - - mWidth = ADisplayConfig_getWidth(mCurrentConfig); - mHeight = ADisplayConfig_getHeight(mCurrentConfig); - mDensity = ADisplayConfig_getDensity(mCurrentConfig); - mVsyncPeriod = static_cast<int64_t>(1000000000 / ADisplayConfig_getFps(mCurrentConfig)); - mCompositorOffset = ADisplayConfig_getCompositorOffsetNanos(mCurrentConfig); - mAppOffset = ADisplayConfig_getAppVsyncOffsetNanos(mCurrentConfig); -} +std::atomic<float> DeviceInfo::sDensity = 2.0; } /* namespace uirenderer */ } /* namespace android */ diff --git a/libs/hwui/DeviceInfo.h b/libs/hwui/DeviceInfo.h index 16a22f4706f5..27be62269959 100644 --- a/libs/hwui/DeviceInfo.h +++ b/libs/hwui/DeviceInfo.h @@ -16,8 +16,10 @@ #ifndef DEVICEINFO_H #define DEVICEINFO_H -#include <apex/display.h> #include <SkImageInfo.h> +#include <android/data_space.h> + +#include <mutex> #include "utils/Macros.h" @@ -36,16 +38,37 @@ public: static float getMaxRefreshRate() { return get()->mMaxRefreshRate; } static int32_t getWidth() { return get()->mWidth; } static int32_t getHeight() { return get()->mHeight; } - static float getDensity() { return get()->mDensity; } + // Gets the density in density-independent pixels + static float getDensity() { return sDensity.load(); } static int64_t getVsyncPeriod() { return get()->mVsyncPeriod; } - static int64_t getCompositorOffset() { return get()->mCompositorOffset; } - static int64_t getAppOffset() { return get()->mAppOffset; } + static int64_t getCompositorOffset() { return get()->getCompositorOffsetInternal(); } + static int64_t getAppOffset() { return get()->mAppVsyncOffsetNanos; } + // Sets the density in density-independent pixels + static void setDensity(float density) { sDensity.store(density); } + static void setMaxRefreshRate(float refreshRate) { get()->mMaxRefreshRate = refreshRate; } + static void setWidth(int32_t width) { get()->mWidth = width; } + static void setHeight(int32_t height) { get()->mHeight = height; } + static void setRefreshRate(float refreshRate) { + get()->mVsyncPeriod = static_cast<int64_t>(1000000000 / refreshRate); + } + static void setPresentationDeadlineNanos(int64_t deadlineNanos) { + get()->mPresentationDeadlineNanos = deadlineNanos; + } + static void setAppVsyncOffsetNanos(int64_t offsetNanos) { + get()->mAppVsyncOffsetNanos = offsetNanos; + } + static void setWideColorDataspace(ADataSpace dataspace); // this value is only valid after the GPU has been initialized and there is a valid graphics // context or if you are using the HWUI_NULL_GPU int maxTextureSize() const; sk_sp<SkColorSpace> getWideColorSpace() const { return mWideColorSpace; } - SkColorType getWideColorType() const { return mWideColorType; } + SkColorType getWideColorType() { + static std::once_flag kFlag; + // lazily update display info from SF here, so that the call is performed by RenderThread. + std::call_once(kFlag, [&, this]() { updateDisplayInfo(); }); + return mWideColorType; + } // This method should be called whenever the display refresh rate changes. void onRefreshRateChanged(int64_t vsyncPeriod); @@ -54,24 +77,32 @@ private: friend class renderthread::RenderThread; static void setMaxTextureSize(int maxTextureSize); void updateDisplayInfo(); + int64_t getCompositorOffsetInternal() const { + // Assume that SF takes around a millisecond to latch buffers after + // waking up + return mVsyncPeriod - (mPresentationDeadlineNanos - 1000000); + } DeviceInfo(); - ~DeviceInfo(); + ~DeviceInfo() = default; int mMaxTextureSize; sk_sp<SkColorSpace> mWideColorSpace = SkColorSpace::MakeSRGB(); SkColorType mWideColorType = SkColorType::kN32_SkColorType; - ADisplayConfig* mCurrentConfig = nullptr; - ADisplay** mDisplays = nullptr; int mDisplaysSize = 0; int mPhysicalDisplayIndex = -1; float mMaxRefreshRate = 60.0; int32_t mWidth = 1080; int32_t mHeight = 1920; - float mDensity = 2.0; int64_t mVsyncPeriod = 16666666; - int64_t mCompositorOffset = 0; - int64_t mAppOffset = 0; + // Magically corresponds with an sf offset of 0 for a sane default. + int64_t mPresentationDeadlineNanos = 17666666; + int64_t mAppVsyncOffsetNanos = 0; + + // Density is not retrieved from the ADisplay apis, so this may potentially + // be called on multiple threads. + // Unit is density-independent pixels + static std::atomic<float> sDensity; }; } /* namespace uirenderer */ diff --git a/libs/hwui/FrameInfo.cpp b/libs/hwui/FrameInfo.cpp index 0698775b0021..fd18d2f9192d 100644 --- a/libs/hwui/FrameInfo.cpp +++ b/libs/hwui/FrameInfo.cpp @@ -22,6 +22,7 @@ namespace uirenderer { const std::string FrameInfoNames[] = { "Flags", + "FrameTimelineVsyncId", "IntendedVsync", "Vsync", "OldestInputEvent", @@ -30,6 +31,7 @@ const std::string FrameInfoNames[] = { "AnimationStart", "PerformTraversalsStart", "DrawStart", + "FrameDeadline", "SyncQueued", "SyncStart", "IssueDrawCommandsStart", @@ -44,7 +46,7 @@ static_assert((sizeof(FrameInfoNames) / sizeof(FrameInfoNames[0])) == static_cast<int>(FrameInfoIndex::NumIndexes), "size mismatch: FrameInfoNames doesn't match the enum!"); -static_assert(static_cast<int>(FrameInfoIndex::NumIndexes) == 17, +static_assert(static_cast<int>(FrameInfoIndex::NumIndexes) == 19, "Must update value in FrameMetrics.java#FRAME_STATS_COUNT (and here)"); void FrameInfo::importUiThreadInfo(int64_t* info) { diff --git a/libs/hwui/FrameInfo.h b/libs/hwui/FrameInfo.h index 51674fbd557e..bb875e35f6f7 100644 --- a/libs/hwui/FrameInfo.h +++ b/libs/hwui/FrameInfo.h @@ -27,10 +27,11 @@ namespace android { namespace uirenderer { -#define UI_THREAD_FRAME_INFO_SIZE 9 +#define UI_THREAD_FRAME_INFO_SIZE 11 enum class FrameInfoIndex { Flags = 0, + FrameTimelineVsyncId, IntendedVsync, Vsync, OldestInputEvent, @@ -39,6 +40,7 @@ enum class FrameInfoIndex { AnimationStart, PerformTraversalsStart, DrawStart, + FrameDeadline, // End of UI frame info SyncQueued, @@ -69,13 +71,19 @@ enum { }; }; -class ANDROID_API UiFrameInfoBuilder { +class UiFrameInfoBuilder { public: + static constexpr int64_t INVALID_VSYNC_ID = -1; + explicit UiFrameInfoBuilder(int64_t* buffer) : mBuffer(buffer) { memset(mBuffer, 0, UI_THREAD_FRAME_INFO_SIZE * sizeof(int64_t)); + set(FrameInfoIndex::FrameTimelineVsyncId) = INVALID_VSYNC_ID; + set(FrameInfoIndex::FrameDeadline) = std::numeric_limits<int64_t>::max(); } - UiFrameInfoBuilder& setVsync(nsecs_t vsyncTime, nsecs_t intendedVsync) { + UiFrameInfoBuilder& setVsync(nsecs_t vsyncTime, nsecs_t intendedVsync, + int64_t vsyncId, int64_t frameDeadline) { + set(FrameInfoIndex::FrameTimelineVsyncId) = vsyncId; set(FrameInfoIndex::Vsync) = vsyncTime; set(FrameInfoIndex::IntendedVsync) = intendedVsync; // Pretend the other fields are all at vsync, too, so that naive @@ -84,6 +92,7 @@ public: set(FrameInfoIndex::AnimationStart) = vsyncTime; set(FrameInfoIndex::PerformTraversalsStart) = vsyncTime; set(FrameInfoIndex::DrawStart) = vsyncTime; + set(FrameInfoIndex::FrameDeadline) = frameDeadline; return *this; } @@ -149,7 +158,7 @@ public: // GPU start time is approximated to the moment before swapBuffer is invoked. // We could add an EGLSyncKHR fence at the beginning of the frame, but that is an overhead. int64_t endTime = get(FrameInfoIndex::GpuCompleted); - return endTime > 0 ? endTime - get(FrameInfoIndex::SwapBuffers) : -1; + return endTime > 0 ? endTime - get(FrameInfoIndex::SwapBuffers) : 0; } inline int64_t& set(FrameInfoIndex index) { return mFrameInfo[static_cast<int>(index)]; } diff --git a/libs/hwui/HardwareBitmapUploader.cpp b/libs/hwui/HardwareBitmapUploader.cpp index a3d552faeb0a..ab9b8b55a4cb 100644 --- a/libs/hwui/HardwareBitmapUploader.cpp +++ b/libs/hwui/HardwareBitmapUploader.cpp @@ -16,24 +16,27 @@ #include "HardwareBitmapUploader.h" -#include "hwui/Bitmap.h" -#include "renderthread/EglManager.h" -#include "renderthread/VulkanManager.h" -#include "thread/ThreadBase.h" -#include "utils/TimeUtils.h" - +#include <EGL/egl.h> #include <EGL/eglext.h> #include <GLES2/gl2.h> #include <GLES2/gl2ext.h> #include <GLES3/gl3.h> -#include <GrContext.h> +#include <GrDirectContext.h> #include <SkCanvas.h> #include <SkImage.h> #include <utils/GLUtils.h> +#include <utils/NdkUtils.h> #include <utils/Trace.h> #include <utils/TraceUtils.h> + #include <thread> +#include "hwui/Bitmap.h" +#include "renderthread/EglManager.h" +#include "renderthread/VulkanManager.h" +#include "thread/ThreadBase.h" +#include "utils/TimeUtils.h" + namespace android::uirenderer { class AHBUploader; @@ -42,7 +45,7 @@ class AHBUploader; static sp<AHBUploader> sUploader = nullptr; struct FormatInfo { - PixelFormat pixelFormat; + AHardwareBuffer_Format bufferFormat; GLint format, type; VkFormat vkFormat; bool isSupported = false; @@ -53,12 +56,6 @@ class AHBUploader : public RefBase { public: virtual ~AHBUploader() {} - // Called to start creation of the Vulkan and EGL contexts on another thread before we actually - // need to do an upload. - void initialize() { - onInitialize(); - } - void destroy() { std::lock_guard _lock{mLock}; LOG_ALWAYS_FATAL_IF(mPendingUploads, "terminate called while uploads in progress"); @@ -71,10 +68,10 @@ public: } bool uploadHardwareBitmap(const SkBitmap& bitmap, const FormatInfo& format, - sp<GraphicBuffer> graphicBuffer) { + AHardwareBuffer* ahb) { ATRACE_CALL(); beginUpload(); - bool result = onUploadHardwareBitmap(bitmap, format, graphicBuffer); + bool result = onUploadHardwareBitmap(bitmap, format, ahb); endUpload(); return result; } @@ -88,12 +85,11 @@ protected: sp<ThreadBase> mUploadThread = nullptr; private: - virtual void onInitialize() = 0; virtual void onIdle() = 0; virtual void onDestroy() = 0; virtual bool onUploadHardwareBitmap(const SkBitmap& bitmap, const FormatInfo& format, - sp<GraphicBuffer> graphicBuffer) = 0; + AHardwareBuffer* ahb) = 0; virtual void onBeginUpload() = 0; bool shouldTimeOutLocked() { @@ -138,7 +134,6 @@ private: class EGLUploader : public AHBUploader { private: - void onInitialize() override {} void onDestroy() override { mEglManager.destroy(); } @@ -165,16 +160,16 @@ private: } bool onUploadHardwareBitmap(const SkBitmap& bitmap, const FormatInfo& format, - sp<GraphicBuffer> graphicBuffer) override { + AHardwareBuffer* ahb) override { ATRACE_CALL(); EGLDisplay display = getUploadEglDisplay(); LOG_ALWAYS_FATAL_IF(display == EGL_NO_DISPLAY, "Failed to get EGL_DEFAULT_DISPLAY! err=%s", uirenderer::renderthread::EglManager::eglErrorString()); - // We use an EGLImage to access the content of the GraphicBuffer + // We use an EGLImage to access the content of the buffer // The EGL image is later bound to a 2D texture - EGLClientBuffer clientBuffer = (EGLClientBuffer)graphicBuffer->getNativeBuffer(); + const EGLClientBuffer clientBuffer = eglGetNativeClientBufferANDROID(ahb); AutoEglImage autoImage(display, clientBuffer); if (autoImage.image == EGL_NO_IMAGE_KHR) { ALOGW("Could not create EGL image, err =%s", @@ -228,62 +223,67 @@ private: class VkUploader : public AHBUploader { private: - void onInitialize() override { - std::lock_guard _lock{mLock}; - if (!mUploadThread) { - mUploadThread = new ThreadBase{}; - } - if (!mUploadThread->isRunning()) { - mUploadThread->start("GrallocUploadThread"); - } - - mUploadThread->queue().post([this]() { - std::lock_guard _lock{mVkLock}; - if (!mVulkanManager.hasVkContext()) { - mVulkanManager.initialize(); - } - }); - } void onDestroy() override { + std::lock_guard _lock{mVkLock}; mGrContext.reset(); - mVulkanManager.destroy(); + mVulkanManagerStrong.clear(); } void onIdle() override { - mGrContext.reset(); + onDestroy(); } - void onBeginUpload() override { - { - std::lock_guard _lock{mVkLock}; - if (!mVulkanManager.hasVkContext()) { - LOG_ALWAYS_FATAL_IF(mGrContext, - "GrContext exists with no VulkanManager for vulkan uploads"); - mUploadThread->queue().runSync([this]() { - mVulkanManager.initialize(); - }); - } - } - if (!mGrContext) { - GrContextOptions options; - mGrContext = mVulkanManager.createContext(options); - LOG_ALWAYS_FATAL_IF(!mGrContext, "failed to create GrContext for vulkan uploads"); - this->postIdleTimeoutCheck(); - } - } + void onBeginUpload() override {} bool onUploadHardwareBitmap(const SkBitmap& bitmap, const FormatInfo& format, - sp<GraphicBuffer> graphicBuffer) override { - ATRACE_CALL(); + AHardwareBuffer* ahb) override { + bool uploadSucceeded = false; + mUploadThread->queue().runSync([this, &uploadSucceeded, bitmap, ahb]() { + ATRACE_CALL(); + std::lock_guard _lock{mVkLock}; + + renderthread::VulkanManager* vkManager = getVulkanManager(); + if (!vkManager->hasVkContext()) { + LOG_ALWAYS_FATAL_IF(mGrContext, + "GrContext exists with no VulkanManager for vulkan uploads"); + vkManager->initialize(); + } + + if (!mGrContext) { + GrContextOptions options; + mGrContext = vkManager->createContext(options, + renderthread::VulkanManager::ContextType::kUploadThread); + LOG_ALWAYS_FATAL_IF(!mGrContext, "failed to create GrContext for vulkan uploads"); + this->postIdleTimeoutCheck(); + } + + sk_sp<SkImage> image = + SkImage::MakeFromAHardwareBufferWithData(mGrContext.get(), bitmap.pixmap(), ahb); + mGrContext->submit(true); + + uploadSucceeded = (image.get() != nullptr); + }); + return uploadSucceeded; + } - std::lock_guard _lock{mLock}; + /* must be called on the upload thread after the vkLock has been acquired */ + renderthread::VulkanManager* getVulkanManager() { + if (!mVulkanManagerStrong) { + mVulkanManagerStrong = mVulkanManagerWeak.promote(); - sk_sp<SkImage> image = SkImage::MakeFromAHardwareBufferWithData(mGrContext.get(), - bitmap.pixmap(), reinterpret_cast<AHardwareBuffer*>(graphicBuffer.get())); - return (image.get() != nullptr); + // create a new manager if we couldn't promote the weak ref + if (!mVulkanManagerStrong) { + mVulkanManagerStrong = renderthread::VulkanManager::getInstance(); + mGrContext.reset(); + mVulkanManagerWeak = mVulkanManagerStrong; + } + } + + return mVulkanManagerStrong.get(); } - sk_sp<GrContext> mGrContext; - renderthread::VulkanManager mVulkanManager; + sk_sp<GrDirectContext> mGrContext; + sp<renderthread::VulkanManager> mVulkanManagerStrong; + wp<renderthread::VulkanManager> mVulkanManagerWeak; std::mutex mVkLock; }; @@ -294,13 +294,17 @@ bool HardwareBitmapUploader::hasFP16Support() { // Gralloc shouldn't let us create a USAGE_HW_TEXTURE if GLES is unable to consume it, so // we don't need to double-check the GLES version/extension. std::call_once(sOnce, []() { - sp<GraphicBuffer> buffer = new GraphicBuffer(1, 1, PIXEL_FORMAT_RGBA_FP16, - GraphicBuffer::USAGE_HW_TEXTURE | - GraphicBuffer::USAGE_SW_WRITE_NEVER | - GraphicBuffer::USAGE_SW_READ_NEVER, - "tempFp16Buffer"); - status_t error = buffer->initCheck(); - hasFP16Support = !error; + AHardwareBuffer_Desc desc = { + .width = 1, + .height = 1, + .layers = 1, + .format = AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT, + .usage = AHARDWAREBUFFER_USAGE_CPU_READ_NEVER | + AHARDWAREBUFFER_USAGE_CPU_WRITE_NEVER | + AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE, + }; + UniqueAHardwareBuffer buffer = allocateAHardwareBuffer(desc); + hasFP16Support = buffer != nullptr; }); return hasFP16Support; @@ -314,7 +318,7 @@ static FormatInfo determineFormat(const SkBitmap& skBitmap, bool usingGL) { [[fallthrough]]; // ARGB_4444 is upconverted to RGBA_8888 case kARGB_4444_SkColorType: - formatInfo.pixelFormat = PIXEL_FORMAT_RGBA_8888; + formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; formatInfo.format = GL_RGBA; formatInfo.type = GL_UNSIGNED_BYTE; formatInfo.vkFormat = VK_FORMAT_R8G8B8A8_UNORM; @@ -323,25 +327,25 @@ static FormatInfo determineFormat(const SkBitmap& skBitmap, bool usingGL) { formatInfo.isSupported = HardwareBitmapUploader::hasFP16Support(); if (formatInfo.isSupported) { formatInfo.type = GL_HALF_FLOAT; - formatInfo.pixelFormat = PIXEL_FORMAT_RGBA_FP16; + formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT; formatInfo.vkFormat = VK_FORMAT_R16G16B16A16_SFLOAT; } else { formatInfo.type = GL_UNSIGNED_BYTE; - formatInfo.pixelFormat = PIXEL_FORMAT_RGBA_8888; + formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; formatInfo.vkFormat = VK_FORMAT_R8G8B8A8_UNORM; } formatInfo.format = GL_RGBA; break; case kRGB_565_SkColorType: formatInfo.isSupported = true; - formatInfo.pixelFormat = PIXEL_FORMAT_RGB_565; + formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM; formatInfo.format = GL_RGB; formatInfo.type = GL_UNSIGNED_SHORT_5_6_5; formatInfo.vkFormat = VK_FORMAT_R5G6B5_UNORM_PACK16; break; case kGray_8_SkColorType: formatInfo.isSupported = usingGL; - formatInfo.pixelFormat = PIXEL_FORMAT_RGBA_8888; + formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; formatInfo.format = GL_LUMINANCE; formatInfo.type = GL_UNSIGNED_BYTE; formatInfo.vkFormat = VK_FORMAT_R8G8B8A8_UNORM; @@ -394,35 +398,33 @@ sk_sp<Bitmap> HardwareBitmapUploader::allocateHardwareBitmap(const SkBitmap& sou } SkBitmap bitmap = makeHwCompatible(format, sourceBitmap); - sp<GraphicBuffer> buffer = new GraphicBuffer( - static_cast<uint32_t>(bitmap.width()), static_cast<uint32_t>(bitmap.height()), - format.pixelFormat, - GraphicBuffer::USAGE_HW_TEXTURE | GraphicBuffer::USAGE_SW_WRITE_NEVER | - GraphicBuffer::USAGE_SW_READ_NEVER, - std::string("Bitmap::allocateHardwareBitmap pid [") + std::to_string(getpid()) + - "]"); - - status_t error = buffer->initCheck(); - if (error < 0) { - ALOGW("createGraphicBuffer() failed in GraphicBuffer.create()"); + AHardwareBuffer_Desc desc = { + .width = static_cast<uint32_t>(bitmap.width()), + .height = static_cast<uint32_t>(bitmap.height()), + .layers = 1, + .format = format.bufferFormat, + .usage = AHARDWAREBUFFER_USAGE_CPU_READ_NEVER | AHARDWAREBUFFER_USAGE_CPU_WRITE_NEVER | + AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE, + }; + UniqueAHardwareBuffer ahb = allocateAHardwareBuffer(desc); + if (!ahb) { + ALOGW("allocateHardwareBitmap() failed in AHardwareBuffer_allocate()"); return nullptr; - } + }; createUploader(usingGL); - if (!sUploader->uploadHardwareBitmap(bitmap, format, buffer)) { + if (!sUploader->uploadHardwareBitmap(bitmap, format, ahb.get())) { return nullptr; } - return Bitmap::createFrom(buffer->toAHardwareBuffer(), bitmap.colorType(), - bitmap.refColorSpace(), bitmap.alphaType(), - Bitmap::computePalette(bitmap)); + return Bitmap::createFrom(ahb.get(), bitmap.colorType(), bitmap.refColorSpace(), + bitmap.alphaType(), Bitmap::computePalette(bitmap)); } void HardwareBitmapUploader::initialize() { bool usingGL = uirenderer::Properties::getRenderPipelineType() == uirenderer::RenderPipelineType::SkiaGL; createUploader(usingGL); - sUploader->initialize(); } void HardwareBitmapUploader::terminate() { diff --git a/libs/hwui/HardwareBitmapUploader.h b/libs/hwui/HardwareBitmapUploader.h index 72243d23dd35..ad7a95a4fa03 100644 --- a/libs/hwui/HardwareBitmapUploader.h +++ b/libs/hwui/HardwareBitmapUploader.h @@ -20,7 +20,7 @@ namespace android::uirenderer { -class ANDROID_API HardwareBitmapUploader { +class HardwareBitmapUploader { public: static void initialize(); static void terminate(); diff --git a/libs/hwui/Interpolator.h b/libs/hwui/Interpolator.h index 452988fc8711..131cc3935f10 100644 --- a/libs/hwui/Interpolator.h +++ b/libs/hwui/Interpolator.h @@ -37,12 +37,12 @@ protected: Interpolator() {} }; -class ANDROID_API AccelerateDecelerateInterpolator : public Interpolator { +class AccelerateDecelerateInterpolator : public Interpolator { public: virtual float interpolate(float input) override; }; -class ANDROID_API AccelerateInterpolator : public Interpolator { +class AccelerateInterpolator : public Interpolator { public: explicit AccelerateInterpolator(float factor) : mFactor(factor), mDoubleFactor(factor * 2) {} virtual float interpolate(float input) override; @@ -52,7 +52,7 @@ private: const float mDoubleFactor; }; -class ANDROID_API AnticipateInterpolator : public Interpolator { +class AnticipateInterpolator : public Interpolator { public: explicit AnticipateInterpolator(float tension) : mTension(tension) {} virtual float interpolate(float input) override; @@ -61,7 +61,7 @@ private: const float mTension; }; -class ANDROID_API AnticipateOvershootInterpolator : public Interpolator { +class AnticipateOvershootInterpolator : public Interpolator { public: explicit AnticipateOvershootInterpolator(float tension) : mTension(tension) {} virtual float interpolate(float input) override; @@ -70,12 +70,12 @@ private: const float mTension; }; -class ANDROID_API BounceInterpolator : public Interpolator { +class BounceInterpolator : public Interpolator { public: virtual float interpolate(float input) override; }; -class ANDROID_API CycleInterpolator : public Interpolator { +class CycleInterpolator : public Interpolator { public: explicit CycleInterpolator(float cycles) : mCycles(cycles) {} virtual float interpolate(float input) override; @@ -84,7 +84,7 @@ private: const float mCycles; }; -class ANDROID_API DecelerateInterpolator : public Interpolator { +class DecelerateInterpolator : public Interpolator { public: explicit DecelerateInterpolator(float factor) : mFactor(factor) {} virtual float interpolate(float input) override; @@ -93,12 +93,12 @@ private: const float mFactor; }; -class ANDROID_API LinearInterpolator : public Interpolator { +class LinearInterpolator : public Interpolator { public: virtual float interpolate(float input) override { return input; } }; -class ANDROID_API OvershootInterpolator : public Interpolator { +class OvershootInterpolator : public Interpolator { public: explicit OvershootInterpolator(float tension) : mTension(tension) {} virtual float interpolate(float input) override; @@ -107,7 +107,7 @@ private: const float mTension; }; -class ANDROID_API PathInterpolator : public Interpolator { +class PathInterpolator : public Interpolator { public: explicit PathInterpolator(std::vector<float>&& x, std::vector<float>&& y) : mX(x), mY(y) {} virtual float interpolate(float input) override; @@ -117,7 +117,7 @@ private: std::vector<float> mY; }; -class ANDROID_API LUTInterpolator : public Interpolator { +class LUTInterpolator : public Interpolator { public: LUTInterpolator(float* values, size_t size); ~LUTInterpolator(); diff --git a/libs/hwui/Matrix.h b/libs/hwui/Matrix.h index 0c515a41689d..4c6e1a0a6eee 100644 --- a/libs/hwui/Matrix.h +++ b/libs/hwui/Matrix.h @@ -44,7 +44,7 @@ namespace uirenderer { // Classes /////////////////////////////////////////////////////////////////////////////// -class ANDROID_API Matrix4 { +class Matrix4 { public: float data[16]; diff --git a/libs/hwui/PathParser.h b/libs/hwui/PathParser.h index 878bb7c0f137..859697eb3e9b 100644 --- a/libs/hwui/PathParser.h +++ b/libs/hwui/PathParser.h @@ -30,17 +30,17 @@ namespace uirenderer { class PathParser { public: - struct ANDROID_API ParseResult { + struct ParseResult { bool failureOccurred = false; std::string failureMessage; }; /** * Parse the string literal and create a Skia Path. Return true on success. */ - ANDROID_API static void parseAsciiStringForSkPath(SkPath* outPath, ParseResult* result, - const char* pathStr, size_t strLength); - ANDROID_API static void getPathDataFromAsciiString(PathData* outData, ParseResult* result, - const char* pathStr, size_t strLength); + static void parseAsciiStringForSkPath(SkPath* outPath, ParseResult* result, + const char* pathStr, size_t strLength); + static void getPathDataFromAsciiString(PathData* outData, ParseResult* result, + const char* pathStr, size_t strLength); static void dump(const PathData& data); static void validateVerbAndPoints(char verb, size_t points, ParseResult* result); }; diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index 446e81e65bb8..ba44d056dda3 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -78,6 +78,7 @@ bool Properties::isolatedProcess = false; int Properties::contextPriority = 0; int Properties::defaultRenderAhead = -1; +float Properties::defaultSdrWhitePoint = 200.f; bool Properties::load() { bool prevDebugLayersUpdates = debugLayersUpdates; diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index d3ecb54d94f6..85a0f4aa7809 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -213,10 +213,10 @@ public: static int overrideSpotShadowStrength; static ProfileType getProfileType(); - ANDROID_API static RenderPipelineType peekRenderPipelineType(); - ANDROID_API static RenderPipelineType getRenderPipelineType(); + static RenderPipelineType peekRenderPipelineType(); + static RenderPipelineType getRenderPipelineType(); - ANDROID_API static bool enableHighContrastText; + static bool enableHighContrastText; // Should be used only by test apps static bool waitForGpuCompletion; @@ -235,20 +235,22 @@ public: static bool skpCaptureEnabled; // For experimentation b/68769804 - ANDROID_API static bool enableRTAnimations; + static bool enableRTAnimations; // Used for testing only to change the render pipeline. static void overrideRenderPipelineType(RenderPipelineType); static bool runningInEmulator; - ANDROID_API static bool debuggingEnabled; - ANDROID_API static bool isolatedProcess; + static bool debuggingEnabled; + static bool isolatedProcess; - ANDROID_API static int contextPriority; + static int contextPriority; static int defaultRenderAhead; + static float defaultSdrWhitePoint; + private: static ProfileType sProfileType; static bool sDisableProfileBars; diff --git a/libs/hwui/PropertyValuesAnimatorSet.h b/libs/hwui/PropertyValuesAnimatorSet.h index e4214b22d1cc..c04a0b9b0fe7 100644 --- a/libs/hwui/PropertyValuesAnimatorSet.h +++ b/libs/hwui/PropertyValuesAnimatorSet.h @@ -44,7 +44,7 @@ private: }; // TODO: This class should really be named VectorDrawableAnimator -class ANDROID_API PropertyValuesAnimatorSet : public BaseRenderNodeAnimator { +class PropertyValuesAnimatorSet : public BaseRenderNodeAnimator { public: friend class PropertyAnimatorSetListener; PropertyValuesAnimatorSet(); diff --git a/libs/hwui/PropertyValuesHolder.h b/libs/hwui/PropertyValuesHolder.h index 0a799d3c0b5c..bb26cbe7bc9b 100644 --- a/libs/hwui/PropertyValuesHolder.h +++ b/libs/hwui/PropertyValuesHolder.h @@ -28,7 +28,7 @@ namespace uirenderer { * When a fraction in [0f, 1f] is provided, the holder will calculate an interpolated value based * on its start and end value, and set the new value on the VectorDrawble's corresponding property. */ -class ANDROID_API PropertyValuesHolder { +class PropertyValuesHolder { public: virtual void setFraction(float fraction) = 0; virtual ~PropertyValuesHolder() {} @@ -49,19 +49,19 @@ public: } }; -class ANDROID_API ColorEvaluator : public Evaluator<SkColor> { +class ColorEvaluator : public Evaluator<SkColor> { public: virtual void evaluate(SkColor* outColor, const SkColor& from, const SkColor& to, float fraction) const override; }; -class ANDROID_API PathEvaluator : public Evaluator<PathData> { +class PathEvaluator : public Evaluator<PathData> { virtual void evaluate(PathData* out, const PathData& from, const PathData& to, float fraction) const override; }; template <typename T> -class ANDROID_API PropertyValuesHolderImpl : public PropertyValuesHolder { +class PropertyValuesHolderImpl : public PropertyValuesHolder { public: PropertyValuesHolderImpl(const T& startValue, const T& endValue) : mStartValue(startValue), mEndValue(endValue) {} @@ -85,7 +85,7 @@ protected: T mEndValue; }; -class ANDROID_API GroupPropertyValuesHolder : public PropertyValuesHolderImpl<float> { +class GroupPropertyValuesHolder : public PropertyValuesHolderImpl<float> { public: GroupPropertyValuesHolder(VectorDrawable::Group* ptr, int propertyId, float startValue, float endValue) @@ -99,7 +99,7 @@ private: int mPropertyId; }; -class ANDROID_API FullPathColorPropertyValuesHolder : public PropertyValuesHolderImpl<SkColor> { +class FullPathColorPropertyValuesHolder : public PropertyValuesHolderImpl<SkColor> { public: FullPathColorPropertyValuesHolder(VectorDrawable::FullPath* ptr, int propertyId, SkColor startValue, SkColor endValue) @@ -116,7 +116,7 @@ private: int mPropertyId; }; -class ANDROID_API FullPathPropertyValuesHolder : public PropertyValuesHolderImpl<float> { +class FullPathPropertyValuesHolder : public PropertyValuesHolderImpl<float> { public: FullPathPropertyValuesHolder(VectorDrawable::FullPath* ptr, int propertyId, float startValue, float endValue) @@ -132,7 +132,7 @@ private: int mPropertyId; }; -class ANDROID_API PathDataPropertyValuesHolder : public PropertyValuesHolderImpl<PathData> { +class PathDataPropertyValuesHolder : public PropertyValuesHolderImpl<PathData> { public: PathDataPropertyValuesHolder(VectorDrawable::Path* ptr, PathData* startValue, PathData* endValue) @@ -146,7 +146,7 @@ private: PathData mPathData; }; -class ANDROID_API RootAlphaPropertyValuesHolder : public PropertyValuesHolderImpl<float> { +class RootAlphaPropertyValuesHolder : public PropertyValuesHolderImpl<float> { public: RootAlphaPropertyValuesHolder(VectorDrawable::Tree* tree, float startValue, float endValue) : PropertyValuesHolderImpl(startValue, endValue), mTree(tree) { diff --git a/libs/hwui/Readback.cpp b/libs/hwui/Readback.cpp index 39900e65cb8a..b71bb07dbc86 100644 --- a/libs/hwui/Readback.cpp +++ b/libs/hwui/Readback.cpp @@ -18,7 +18,6 @@ #include <sync/sync.h> #include <system/window.h> -#include <ui/GraphicBuffer.h> #include "DeferredLayerUpdater.h" #include "Properties.h" @@ -28,6 +27,7 @@ #include "renderthread/VulkanManager.h" #include "utils/Color.h" #include "utils/MathUtils.h" +#include "utils/NdkUtils.h" #include "utils/TraceUtils.h" using namespace android::uirenderer::renderthread; @@ -54,8 +54,7 @@ CopyResult Readback::copySurfaceInto(ANativeWindow* window, const Rect& srcRect, return CopyResult::SourceEmpty; } - std::unique_ptr<AHardwareBuffer, decltype(&AHardwareBuffer_release)> sourceBuffer( - rawSourceBuffer, AHardwareBuffer_release); + UniqueAHardwareBuffer sourceBuffer{rawSourceBuffer}; AHardwareBuffer_Desc description; AHardwareBuffer_describe(sourceBuffer.get(), &description); if (description.usage & AHARDWAREBUFFER_USAGE_PROTECTED_CONTENT) { @@ -119,7 +118,7 @@ CopyResult Readback::copyImageInto(const sk_sp<SkImage>& image, Matrix4& texTran } int imgWidth = image->width(); int imgHeight = image->height(); - sk_sp<GrContext> grContext = sk_ref_sp(mRenderThread.getGrContext()); + sk_sp<GrDirectContext> grContext = sk_ref_sp(mRenderThread.getGrContext()); if (bitmap->colorType() == kRGBA_F16_SkColorType && !grContext->colorTypeSupportedAsSurface(bitmap->colorType())) { diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp index dc467c41baed..473dc53dc4bf 100644 --- a/libs/hwui/RecordingCanvas.cpp +++ b/libs/hwui/RecordingCanvas.cpp @@ -96,7 +96,7 @@ struct Restore final : Op { struct SaveLayer final : Op { static const auto kType = Type::SaveLayer; SaveLayer(const SkRect* bounds, const SkPaint* paint, const SkImageFilter* backdrop, - const SkImage* clipMask, const SkMatrix* clipMatrix, SkCanvas::SaveLayerFlags flags) { + SkCanvas::SaveLayerFlags flags) { if (bounds) { this->bounds = *bounds; } @@ -104,19 +104,14 @@ struct SaveLayer final : Op { this->paint = *paint; } this->backdrop = sk_ref_sp(backdrop); - this->clipMask = sk_ref_sp(clipMask); - this->clipMatrix = clipMatrix ? *clipMatrix : SkMatrix::I(); this->flags = flags; } SkRect bounds = kUnset; SkPaint paint; sk_sp<const SkImageFilter> backdrop; - sk_sp<const SkImage> clipMask; - SkMatrix clipMatrix; SkCanvas::SaveLayerFlags flags; void draw(SkCanvas* c, const SkMatrix&) const { - c->saveLayer({maybe_unset(bounds), &paint, backdrop.get(), clipMask.get(), - clipMatrix.isIdentity() ? nullptr : &clipMatrix, flags}); + c->saveLayer({maybe_unset(bounds), &paint, backdrop.get(), flags}); } }; struct SaveBehind final : Op { @@ -132,9 +127,9 @@ struct SaveBehind final : Op { struct Concat44 final : Op { static const auto kType = Type::Concat44; - Concat44(const SkScalar m[16]) { memcpy(colMajor, m, sizeof(colMajor)); } - SkScalar colMajor[16]; - void draw(SkCanvas* c, const SkMatrix&) const { c->experimental_concat44(colMajor); } + Concat44(const SkM44& m) : matrix(m) {} + SkM44 matrix; + void draw(SkCanvas* c, const SkMatrix&) const { c->concat(matrix); } }; struct Concat final : Op { static const auto kType = Type::Concat; @@ -448,14 +443,13 @@ struct DrawPoints final : Op { }; struct DrawVertices final : Op { static const auto kType = Type::DrawVertices; - DrawVertices(const SkVertices* v, int bc, SkBlendMode m, const SkPaint& p) - : vertices(sk_ref_sp(const_cast<SkVertices*>(v))), boneCount(bc), mode(m), paint(p) {} + DrawVertices(const SkVertices* v, SkBlendMode m, const SkPaint& p) + : vertices(sk_ref_sp(const_cast<SkVertices*>(v))), mode(m), paint(p) {} sk_sp<SkVertices> vertices; - int boneCount; SkBlendMode mode; SkPaint paint; void draw(SkCanvas* c, const SkMatrix&) const { - c->drawVertices(vertices, pod<SkVertices::Bone>(this), boneCount, mode, paint); + c->drawVertices(vertices, mode, paint); } }; struct DrawAtlas final : Op { @@ -530,6 +524,7 @@ void* DisplayListData::push(size_t pod, Args&&... args) { // Next greater multiple of SKLITEDL_PAGE. fReserved = (fUsed + skip + SKLITEDL_PAGE) & ~(SKLITEDL_PAGE - 1); fBytes.realloc(fReserved); + LOG_ALWAYS_FATAL_IF(fBytes.get() == nullptr, "realloc(%zd) failed", fReserved); } SkASSERT(fUsed + skip <= fReserved); auto op = (T*)(fBytes.get() + fUsed); @@ -565,17 +560,16 @@ void DisplayListData::restore() { this->push<Restore>(0); } void DisplayListData::saveLayer(const SkRect* bounds, const SkPaint* paint, - const SkImageFilter* backdrop, const SkImage* clipMask, - const SkMatrix* clipMatrix, SkCanvas::SaveLayerFlags flags) { - this->push<SaveLayer>(0, bounds, paint, backdrop, clipMask, clipMatrix, flags); + const SkImageFilter* backdrop, SkCanvas::SaveLayerFlags flags) { + this->push<SaveLayer>(0, bounds, paint, backdrop, flags); } void DisplayListData::saveBehind(const SkRect* subset) { this->push<SaveBehind>(0, subset); } -void DisplayListData::concat44(const SkScalar colMajor[16]) { - this->push<Concat44>(0, colMajor); +void DisplayListData::concat(const SkM44& m) { + this->push<Concat44>(0, m); } void DisplayListData::concat(const SkMatrix& matrix) { this->push<Concat>(0, matrix); @@ -686,11 +680,8 @@ void DisplayListData::drawPoints(SkCanvas::PointMode mode, size_t count, const S void* pod = this->push<DrawPoints>(count * sizeof(SkPoint), mode, count, paint); copy_v(pod, points, count); } -void DisplayListData::drawVertices(const SkVertices* vertices, const SkVertices::Bone bones[], - int boneCount, SkBlendMode mode, const SkPaint& paint) { - void* pod = this->push<DrawVertices>(boneCount * sizeof(SkVertices::Bone), vertices, boneCount, - mode, paint); - copy_v(pod, bones, boneCount); +void DisplayListData::drawVertices(const SkVertices* vertices, SkBlendMode mode, const SkPaint& paint) { + this->push<DrawVertices>(0, vertices, mode, paint); } void DisplayListData::drawAtlas(const SkImage* atlas, const SkRSXform xforms[], const SkRect texs[], const SkColor colors[], int count, SkBlendMode xfermode, @@ -823,8 +814,7 @@ void RecordingCanvas::willSave() { fDL->save(); } SkCanvas::SaveLayerStrategy RecordingCanvas::getSaveLayerStrategy(const SaveLayerRec& rec) { - fDL->saveLayer(rec.fBounds, rec.fPaint, rec.fBackdrop, rec.fClipMask, rec.fClipMatrix, - rec.fSaveLayerFlags); + fDL->saveLayer(rec.fBounds, rec.fPaint, rec.fBackdrop, rec.fSaveLayerFlags); return SkCanvas::kNoLayer_SaveLayerStrategy; } void RecordingCanvas::willRestore() { @@ -841,8 +831,8 @@ bool RecordingCanvas::onDoSaveBehind(const SkRect* subset) { return false; } -void RecordingCanvas::didConcat44(const SkScalar colMajor[16]) { - fDL->concat44(colMajor); +void RecordingCanvas::didConcat44(const SkM44& m) { + fDL->concat(m); } void RecordingCanvas::didConcat(const SkMatrix& matrix) { fDL->concat(matrix); @@ -929,24 +919,6 @@ void RecordingCanvas::onDrawTextBlob(const SkTextBlob* blob, SkScalar x, SkScala fDL->drawTextBlob(blob, x, y, paint); } -void RecordingCanvas::onDrawBitmap(const SkBitmap& bm, SkScalar x, SkScalar y, - const SkPaint* paint) { - fDL->drawImage(SkImage::MakeFromBitmap(bm), x, y, paint, BitmapPalette::Unknown); -} -void RecordingCanvas::onDrawBitmapNine(const SkBitmap& bm, const SkIRect& center, const SkRect& dst, - const SkPaint* paint) { - fDL->drawImageNine(SkImage::MakeFromBitmap(bm), center, dst, paint); -} -void RecordingCanvas::onDrawBitmapRect(const SkBitmap& bm, const SkRect* src, const SkRect& dst, - const SkPaint* paint, SrcRectConstraint constraint) { - fDL->drawImageRect(SkImage::MakeFromBitmap(bm), src, dst, paint, constraint, - BitmapPalette::Unknown); -} -void RecordingCanvas::onDrawBitmapLattice(const SkBitmap& bm, const SkCanvas::Lattice& lattice, - const SkRect& dst, const SkPaint* paint) { - fDL->drawImageLattice(SkImage::MakeFromBitmap(bm), lattice, dst, paint, BitmapPalette::Unknown); -} - void RecordingCanvas::drawImage(const sk_sp<SkImage>& image, SkScalar x, SkScalar y, const SkPaint* paint, BitmapPalette palette) { fDL->drawImage(image, x, y, paint, palette); @@ -1007,9 +979,8 @@ void RecordingCanvas::onDrawPoints(SkCanvas::PointMode mode, size_t count, const fDL->drawPoints(mode, count, pts, paint); } void RecordingCanvas::onDrawVerticesObject(const SkVertices* vertices, - const SkVertices::Bone bones[], int boneCount, SkBlendMode mode, const SkPaint& paint) { - fDL->drawVertices(vertices, bones, boneCount, mode, paint); + fDL->drawVertices(vertices, mode, paint); } void RecordingCanvas::onDrawAtlas(const SkImage* atlas, const SkRSXform xforms[], const SkRect texs[], const SkColor colors[], int count, diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h index 7eb1ce3eb18a..63d120c4ca19 100644 --- a/libs/hwui/RecordingCanvas.h +++ b/libs/hwui/RecordingCanvas.h @@ -77,12 +77,11 @@ private: void flush(); void save(); - void saveLayer(const SkRect*, const SkPaint*, const SkImageFilter*, const SkImage*, - const SkMatrix*, SkCanvas::SaveLayerFlags); + void saveLayer(const SkRect*, const SkPaint*, const SkImageFilter*, SkCanvas::SaveLayerFlags); void saveBehind(const SkRect*); void restore(); - void concat44(const SkScalar colMajor[16]); + void concat(const SkM44&); void concat(const SkMatrix&); void setMatrix(const SkMatrix&); void scale(SkScalar, SkScalar); @@ -120,8 +119,7 @@ private: void drawPatch(const SkPoint[12], const SkColor[4], const SkPoint[4], SkBlendMode, const SkPaint&); void drawPoints(SkCanvas::PointMode, size_t, const SkPoint[], const SkPaint&); - void drawVertices(const SkVertices*, const SkVertices::Bone bones[], int boneCount, SkBlendMode, - const SkPaint&); + void drawVertices(const SkVertices*, SkBlendMode, const SkPaint&); void drawAtlas(const SkImage*, const SkRSXform[], const SkRect[], const SkColor[], int, SkBlendMode, const SkRect*, const SkPaint*); void drawShadowRec(const SkPath&, const SkDrawShadowRec&); @@ -155,7 +153,7 @@ public: void onFlush() override; - void didConcat44(const SkScalar[16]) override; + void didConcat44(const SkM44&) override; void didConcat(const SkMatrix&) override; void didSetMatrix(const SkMatrix&) override; void didScale(SkScalar, SkScalar) override; @@ -182,13 +180,6 @@ public: void onDrawTextBlob(const SkTextBlob*, SkScalar, SkScalar, const SkPaint&) override; - void onDrawBitmap(const SkBitmap&, SkScalar, SkScalar, const SkPaint*) override; - void onDrawBitmapLattice(const SkBitmap&, const Lattice&, const SkRect&, - const SkPaint*) override; - void onDrawBitmapNine(const SkBitmap&, const SkIRect&, const SkRect&, const SkPaint*) override; - void onDrawBitmapRect(const SkBitmap&, const SkRect*, const SkRect&, const SkPaint*, - SrcRectConstraint) override; - void drawImage(const sk_sp<SkImage>& image, SkScalar left, SkScalar top, const SkPaint* paint, BitmapPalette pallete); @@ -206,8 +197,7 @@ public: void onDrawPatch(const SkPoint[12], const SkColor[4], const SkPoint[4], SkBlendMode, const SkPaint&) override; void onDrawPoints(PointMode, size_t count, const SkPoint pts[], const SkPaint&) override; - void onDrawVerticesObject(const SkVertices*, const SkVertices::Bone bones[], int boneCount, - SkBlendMode, const SkPaint&) override; + void onDrawVerticesObject(const SkVertices*, SkBlendMode, const SkPaint&) override; void onDrawAtlas(const SkImage*, const SkRSXform[], const SkRect[], const SkColor[], int, SkBlendMode, const SkRect*, const SkPaint*) override; void onDrawShadowRec(const SkPath&, const SkDrawShadowRec&) override; diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h index c0ec2174bb35..6d5e62e955bb 100644 --- a/libs/hwui/RenderNode.h +++ b/libs/hwui/RenderNode.h @@ -94,17 +94,17 @@ public: DISPLAY_LIST = 1 << 14, }; - ANDROID_API RenderNode(); - ANDROID_API virtual ~RenderNode(); + RenderNode(); + virtual ~RenderNode(); // See flags defined in DisplayList.java enum ReplayFlag { kReplayFlag_ClipChildren = 0x1 }; - ANDROID_API void setStagingDisplayList(DisplayList* newData); + void setStagingDisplayList(DisplayList* newData); - ANDROID_API void output(); - ANDROID_API int getUsageSize(); - ANDROID_API int getAllocatedSize(); + void output(); + int getUsageSize(); + int getAllocatedSize(); bool isRenderable() const { return mDisplayList && !mDisplayList->isEmpty(); } @@ -149,12 +149,12 @@ public: int getHeight() const { return properties().getHeight(); } - ANDROID_API virtual void prepareTree(TreeInfo& info); + virtual void prepareTree(TreeInfo& info); void destroyHardwareResources(TreeInfo* info = nullptr); void destroyLayers(); // UI thread only! - ANDROID_API void addAnimator(const sp<BaseRenderNodeAnimator>& animator); + void addAnimator(const sp<BaseRenderNodeAnimator>& animator); void removeAnimator(const sp<BaseRenderNodeAnimator>& animator); // This can only happen during pushStaging() @@ -179,7 +179,7 @@ public: // the frameNumber to appropriately batch/synchronize these transactions. // There is no other filtering/batching to ensure that only the "final" // state called once per frame. - class ANDROID_API PositionListener : public VirtualLightRefBase { + class PositionListener : public VirtualLightRefBase { public: virtual ~PositionListener() {} // Called when the RenderNode's position changes @@ -190,14 +190,14 @@ public: virtual void onPositionLost(RenderNode& node, const TreeInfo* info) = 0; }; - ANDROID_API void setPositionListener(PositionListener* listener) { + void setPositionListener(PositionListener* listener) { mStagingPositionListener = listener; mPositionListenerDirty = true; } // This is only modified in MODE_FULL, so it can be safely accessed // on the UI thread. - ANDROID_API bool hasParents() { return mParentCount; } + bool hasParents() { return mParentCount; } void onRemovedFromTree(TreeInfo* info); diff --git a/libs/hwui/RenderProperties.cpp b/libs/hwui/RenderProperties.cpp index ff9cf45cdc73..8fba9cf21df1 100644 --- a/libs/hwui/RenderProperties.cpp +++ b/libs/hwui/RenderProperties.cpp @@ -49,6 +49,12 @@ bool LayerProperties::setColorFilter(SkColorFilter* filter) { return true; } +bool LayerProperties::setImageFilter(SkImageFilter* imageFilter) { + if(mImageFilter.get() == imageFilter) return false; + mImageFilter = sk_ref_sp(imageFilter); + return true; +} + bool LayerProperties::setFromPaint(const SkPaint* paint) { bool changed = false; changed |= setAlpha(static_cast<uint8_t>(PaintUtils::getAlphaDirect(paint))); @@ -63,6 +69,7 @@ LayerProperties& LayerProperties::operator=(const LayerProperties& other) { setAlpha(other.alpha()); setXferMode(other.xferMode()); setColorFilter(other.getColorFilter()); + setImageFilter(other.getImageFilter()); return *this; } diff --git a/libs/hwui/RenderProperties.h b/libs/hwui/RenderProperties.h index 24f6035b6708..aeb60e6ce355 100644 --- a/libs/hwui/RenderProperties.h +++ b/libs/hwui/RenderProperties.h @@ -27,6 +27,7 @@ #include "utils/PaintUtils.h" #include <SkBlendMode.h> +#include <SkImageFilter.h> #include <SkCamera.h> #include <SkColor.h> #include <SkMatrix.h> @@ -69,7 +70,7 @@ enum ClippingFlags { CLIP_TO_CLIP_BOUNDS = 0x1 << 1, }; -class ANDROID_API LayerProperties { +class LayerProperties { public: bool setType(LayerType type) { if (RP_SET(mType, type)) { @@ -93,6 +94,10 @@ public: SkColorFilter* getColorFilter() const { return mColorFilter.get(); } + bool setImageFilter(SkImageFilter* imageFilter); + + SkImageFilter* getImageFilter() const { return mImageFilter.get(); } + // Sets alpha, xfermode, and colorfilter from an SkPaint // paint may be NULL, in which case defaults will be set bool setFromPaint(const SkPaint* paint); @@ -118,12 +123,13 @@ private: uint8_t mAlpha; SkBlendMode mMode; sk_sp<SkColorFilter> mColorFilter; + sk_sp<SkImageFilter> mImageFilter; }; /* * Data structure that holds the properties for a RenderNode */ -class ANDROID_API RenderProperties { +class RenderProperties { public: RenderProperties(); virtual ~RenderProperties(); @@ -541,6 +547,7 @@ public: bool promotedToLayer() const { return mLayerProperties.mType == LayerType::None && fitsOnLayer() && (mComputedFields.mNeedLayerForFunctors || + mLayerProperties.mImageFilter != nullptr || (!MathUtils::isZero(mPrimitiveFields.mAlpha) && mPrimitiveFields.mAlpha < 1 && mPrimitiveFields.mHasOverlappingRendering)); } diff --git a/libs/hwui/RootRenderNode.h b/libs/hwui/RootRenderNode.h index 12de4ecac94b..1d3f5a8a51e0 100644 --- a/libs/hwui/RootRenderNode.h +++ b/libs/hwui/RootRenderNode.h @@ -27,16 +27,16 @@ namespace android::uirenderer { -class ANDROID_API RootRenderNode : public RenderNode { +class RootRenderNode : public RenderNode { public: - ANDROID_API explicit RootRenderNode(std::unique_ptr<ErrorHandler> errorHandler) + explicit RootRenderNode(std::unique_ptr<ErrorHandler> errorHandler) : RenderNode(), mErrorHandler(std::move(errorHandler)) {} - ANDROID_API virtual ~RootRenderNode() {} + virtual ~RootRenderNode() {} virtual void prepareTree(TreeInfo& info) override; - ANDROID_API void attachAnimatingNode(RenderNode* animatingNode); + void attachAnimatingNode(RenderNode* animatingNode); void attachPendingVectorDrawableAnimators(); @@ -53,9 +53,9 @@ public: void pushStagingVectorDrawableAnimators(AnimationContext* context); - ANDROID_API void destroy(); + void destroy(); - ANDROID_API void addVectorDrawableAnimator(PropertyValuesAnimatorSet* anim); + void addVectorDrawableAnimator(PropertyValuesAnimatorSet* anim); private: const std::unique_ptr<ErrorHandler> mErrorHandler; @@ -75,12 +75,11 @@ private: }; #ifdef __ANDROID__ // Layoutlib does not support Animations -class ANDROID_API ContextFactoryImpl : public IContextFactory { +class ContextFactoryImpl : public IContextFactory { public: - ANDROID_API explicit ContextFactoryImpl(RootRenderNode* rootNode) : mRootNode(rootNode) {} + explicit ContextFactoryImpl(RootRenderNode* rootNode) : mRootNode(rootNode) {} - ANDROID_API virtual AnimationContext* createAnimationContext( - renderthread::TimeLord& clock) override; + virtual AnimationContext* createAnimationContext(renderthread::TimeLord& clock) override; private: RootRenderNode* mRootNode; diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp index 941437998838..1dbce58fb7c9 100644 --- a/libs/hwui/SkiaCanvas.cpp +++ b/libs/hwui/SkiaCanvas.cpp @@ -40,6 +40,7 @@ #include <SkShader.h> #include <SkTemplates.h> #include <SkTextBlob.h> +#include <SkVertices.h> #include <memory> #include <optional> @@ -729,8 +730,7 @@ void SkiaCanvas::drawVectorDrawable(VectorDrawableRoot* vectorDrawable) { // ---------------------------------------------------------------------------- void SkiaCanvas::drawGlyphs(ReadGlyphFunc glyphFunc, int count, const Paint& paint, float x, - float y, float boundsLeft, float boundsTop, float boundsRight, - float boundsBottom, float totalAdvance) { + float y, float totalAdvance) { if (count <= 0 || paint.nothingToDraw()) return; Paint paintCopy(paint); if (mPaintFilter) { @@ -842,9 +842,4 @@ void SkiaCanvas::drawRenderNode(uirenderer::RenderNode* renderNode) { LOG_ALWAYS_FATAL("SkiaCanvas can't directly draw RenderNodes"); } -void SkiaCanvas::callDrawGLFunction(Functor* functor, - uirenderer::GlFunctorLifecycleListener* listener) { - LOG_ALWAYS_FATAL("SkiaCanvas can't directly draw GL Content"); -} - } // namespace android diff --git a/libs/hwui/SkiaCanvas.h b/libs/hwui/SkiaCanvas.h index 1eb089d8764c..2cb850c83934 100644 --- a/libs/hwui/SkiaCanvas.h +++ b/libs/hwui/SkiaCanvas.h @@ -57,8 +57,8 @@ public: LOG_ALWAYS_FATAL("SkiaCanvas does not produce a DisplayList"); return nullptr; } - virtual void insertReorderBarrier(bool enableReorder) override { - LOG_ALWAYS_FATAL("SkiaCanvas does not support reordering barriers"); + virtual void enableZ(bool enableZ) override { + LOG_ALWAYS_FATAL("SkiaCanvas does not support enableZ"); } virtual void setBitmap(const SkBitmap& bitmap) override; @@ -152,8 +152,6 @@ public: virtual void drawLayer(uirenderer::DeferredLayerUpdater* layerHandle) override; virtual void drawRenderNode(uirenderer::RenderNode* renderNode) override; - virtual void callDrawGLFunction(Functor* functor, - uirenderer::GlFunctorLifecycleListener* listener) override; virtual void drawPicture(const SkPicture& picture) override; protected: @@ -163,8 +161,7 @@ protected: void drawDrawable(SkDrawable* drawable) { mCanvas->drawDrawable(drawable); } virtual void drawGlyphs(ReadGlyphFunc glyphFunc, int count, const Paint& paint, float x, - float y, float boundsLeft, float boundsTop, float boundsRight, - float boundsBottom, float totalAdvance) override; + float y, float totalAdvance) override; virtual void drawLayoutOnPath(const minikin::Layout& layout, float hOffset, float vOffset, const Paint& paint, const SkPath& path, size_t start, size_t end) override; diff --git a/libs/hwui/VectorDrawable.h b/libs/hwui/VectorDrawable.h index e1b6f2adde74..ac7d41e0d600 100644 --- a/libs/hwui/VectorDrawable.h +++ b/libs/hwui/VectorDrawable.h @@ -97,7 +97,7 @@ private: bool* mStagingDirty; }; -class ANDROID_API Node { +class Node { public: class Properties { public: @@ -127,9 +127,9 @@ protected: PropertyChangedListener* mPropertyChangedListener = nullptr; }; -class ANDROID_API Path : public Node { +class Path : public Node { public: - struct ANDROID_API Data { + struct Data { std::vector<char> verbs; std::vector<size_t> verbSizes; std::vector<float> points; @@ -200,7 +200,7 @@ private: bool mStagingPropertiesDirty = true; }; -class ANDROID_API FullPath : public Path { +class FullPath : public Path { public: class FullPathProperties : public Properties { public: @@ -369,7 +369,7 @@ private: bool mAntiAlias = true; }; -class ANDROID_API ClipPath : public Path { +class ClipPath : public Path { public: ClipPath(const ClipPath& path) : Path(path) {} ClipPath(const char* path, size_t strLength) : Path(path, strLength) {} @@ -378,7 +378,7 @@ public: virtual void setAntiAlias(bool aa) {} }; -class ANDROID_API Group : public Node { +class Group : public Node { public: class GroupProperties : public Properties { public: @@ -498,7 +498,7 @@ private: std::vector<std::unique_ptr<Node> > mChildren; }; -class ANDROID_API Tree : public VirtualLightRefBase { +class Tree : public VirtualLightRefBase { public: explicit Tree(Group* rootNode) : mRootNode(rootNode) { mRootNode->setPropertyChangedListener(&mPropertyChangedListener); diff --git a/libs/hwui/apex/LayoutlibLoader.cpp b/libs/hwui/apex/LayoutlibLoader.cpp index 4bbf1214bdcf..dca10e29cbb8 100644 --- a/libs/hwui/apex/LayoutlibLoader.cpp +++ b/libs/hwui/apex/LayoutlibLoader.cpp @@ -47,6 +47,7 @@ extern int register_android_graphics_MaskFilter(JNIEnv* env); extern int register_android_graphics_NinePatch(JNIEnv*); extern int register_android_graphics_PathEffect(JNIEnv* env); extern int register_android_graphics_Shader(JNIEnv* env); +extern int register_android_graphics_RenderEffect(JNIEnv* env); extern int register_android_graphics_Typeface(JNIEnv* env); namespace android { @@ -108,6 +109,7 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = { {"android.graphics.RecordingCanvas", REG_JNI(register_android_view_DisplayListCanvas)}, // {"android.graphics.Region", REG_JNI(register_android_graphics_Region)}, {"android.graphics.Shader", REG_JNI(register_android_graphics_Shader)}, + {"android.graphics.RenderEffect", REG_JNI(register_android_graphics_RenderEffect)}, {"android.graphics.Typeface", REG_JNI(register_android_graphics_Typeface)}, {"android.graphics.animation.NativeInterpolatorFactory", REG_JNI(register_android_graphics_animation_NativeInterpolatorFactory)}, diff --git a/libs/hwui/apex/java/android/graphics/ColorMatrix.java b/libs/hwui/apex/java/android/graphics/ColorMatrix.java new file mode 100644 index 000000000000..6299b2c47ea1 --- /dev/null +++ b/libs/hwui/apex/java/android/graphics/ColorMatrix.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import java.util.Arrays; + +/** + * 4x5 matrix for transforming the color and alpha components of a Bitmap. + * The matrix can be passed as single array, and is treated as follows: + * + * <pre> + * [ a, b, c, d, e, + * f, g, h, i, j, + * k, l, m, n, o, + * p, q, r, s, t ]</pre> + * + * <p> + * When applied to a color <code>[R, G, B, A]</code>, the resulting color + * is computed as: + * </p> + * + * <pre> + * R’ = a*R + b*G + c*B + d*A + e; + * G’ = f*R + g*G + h*B + i*A + j; + * B’ = k*R + l*G + m*B + n*A + o; + * A’ = p*R + q*G + r*B + s*A + t;</pre> + * + * <p> + * That resulting color <code>[R’, G’, B’, A’]</code> + * then has each channel clamped to the <code>0</code> to <code>255</code> + * range. + * </p> + * + * <p> + * The sample ColorMatrix below inverts incoming colors by scaling each + * channel by <code>-1</code>, and then shifting the result up by + * <code>255</code> to remain in the standard color space. + * </p> + * + * <pre> + * [ -1, 0, 0, 0, 255, + * 0, -1, 0, 0, 255, + * 0, 0, -1, 0, 255, + * 0, 0, 0, 1, 0 ]</pre> + */ +@SuppressWarnings({ "MismatchedReadAndWriteOfArray", "PointlessArithmeticExpression" }) +public class ColorMatrix { + private final float[] mArray = new float[20]; + + /** + * Create a new colormatrix initialized to identity (as if reset() had + * been called). + */ + public ColorMatrix() { + reset(); + } + + /** + * Create a new colormatrix initialized with the specified array of values. + */ + public ColorMatrix(float[] src) { + System.arraycopy(src, 0, mArray, 0, 20); + } + + /** + * Create a new colormatrix initialized with the specified colormatrix. + */ + public ColorMatrix(ColorMatrix src) { + System.arraycopy(src.mArray, 0, mArray, 0, 20); + } + + /** + * Return the array of floats representing this colormatrix. + */ + public final float[] getArray() { return mArray; } + + /** + * Set this colormatrix to identity: + * <pre> + * [ 1 0 0 0 0 - red vector + * 0 1 0 0 0 - green vector + * 0 0 1 0 0 - blue vector + * 0 0 0 1 0 ] - alpha vector + * </pre> + */ + public void reset() { + final float[] a = mArray; + Arrays.fill(a, 0); + a[0] = a[6] = a[12] = a[18] = 1; + } + + /** + * Assign the src colormatrix into this matrix, copying all of its values. + */ + public void set(ColorMatrix src) { + System.arraycopy(src.mArray, 0, mArray, 0, 20); + } + + /** + * Assign the array of floats into this matrix, copying all of its values. + */ + public void set(float[] src) { + System.arraycopy(src, 0, mArray, 0, 20); + } + + /** + * Set this colormatrix to scale by the specified values. + */ + public void setScale(float rScale, float gScale, float bScale, + float aScale) { + final float[] a = mArray; + + for (int i = 19; i > 0; --i) { + a[i] = 0; + } + a[0] = rScale; + a[6] = gScale; + a[12] = bScale; + a[18] = aScale; + } + + /** + * Set the rotation on a color axis by the specified values. + * <p> + * <code>axis=0</code> correspond to a rotation around the RED color + * <code>axis=1</code> correspond to a rotation around the GREEN color + * <code>axis=2</code> correspond to a rotation around the BLUE color + * </p> + */ + public void setRotate(int axis, float degrees) { + reset(); + double radians = degrees * Math.PI / 180d; + float cosine = (float) Math.cos(radians); + float sine = (float) Math.sin(radians); + switch (axis) { + // Rotation around the red color + case 0: + mArray[6] = mArray[12] = cosine; + mArray[7] = sine; + mArray[11] = -sine; + break; + // Rotation around the green color + case 1: + mArray[0] = mArray[12] = cosine; + mArray[2] = -sine; + mArray[10] = sine; + break; + // Rotation around the blue color + case 2: + mArray[0] = mArray[6] = cosine; + mArray[1] = sine; + mArray[5] = -sine; + break; + default: + throw new RuntimeException(); + } + } + + /** + * Set this colormatrix to the concatenation of the two specified + * colormatrices, such that the resulting colormatrix has the same effect + * as applying matB and then applying matA. + * <p> + * It is legal for either matA or matB to be the same colormatrix as this. + * </p> + */ + public void setConcat(ColorMatrix matA, ColorMatrix matB) { + float[] tmp; + if (matA == this || matB == this) { + tmp = new float[20]; + } else { + tmp = mArray; + } + + final float[] a = matA.mArray; + final float[] b = matB.mArray; + int index = 0; + for (int j = 0; j < 20; j += 5) { + for (int i = 0; i < 4; i++) { + tmp[index++] = a[j + 0] * b[i + 0] + a[j + 1] * b[i + 5] + + a[j + 2] * b[i + 10] + a[j + 3] * b[i + 15]; + } + tmp[index++] = a[j + 0] * b[4] + a[j + 1] * b[9] + + a[j + 2] * b[14] + a[j + 3] * b[19] + + a[j + 4]; + } + + if (tmp != mArray) { + System.arraycopy(tmp, 0, mArray, 0, 20); + } + } + + /** + * Concat this colormatrix with the specified prematrix. + * <p> + * This is logically the same as calling setConcat(this, prematrix); + * </p> + */ + public void preConcat(ColorMatrix prematrix) { + setConcat(this, prematrix); + } + + /** + * Concat this colormatrix with the specified postmatrix. + * <p> + * This is logically the same as calling setConcat(postmatrix, this); + * </p> + */ + public void postConcat(ColorMatrix postmatrix) { + setConcat(postmatrix, this); + } + + /////////////////////////////////////////////////////////////////////////// + + /** + * Set the matrix to affect the saturation of colors. + * + * @param sat A value of 0 maps the color to gray-scale. 1 is identity. + */ + public void setSaturation(float sat) { + reset(); + float[] m = mArray; + + final float invSat = 1 - sat; + final float R = 0.213f * invSat; + final float G = 0.715f * invSat; + final float B = 0.072f * invSat; + + m[0] = R + sat; m[1] = G; m[2] = B; + m[5] = R; m[6] = G + sat; m[7] = B; + m[10] = R; m[11] = G; m[12] = B + sat; + } + + /** + * Set the matrix to convert RGB to YUV + */ + public void setRGB2YUV() { + reset(); + float[] m = mArray; + // these coefficients match those in libjpeg + m[0] = 0.299f; m[1] = 0.587f; m[2] = 0.114f; + m[5] = -0.16874f; m[6] = -0.33126f; m[7] = 0.5f; + m[10] = 0.5f; m[11] = -0.41869f; m[12] = -0.08131f; + } + + /** + * Set the matrix to convert from YUV to RGB + */ + public void setYUV2RGB() { + reset(); + float[] m = mArray; + // these coefficients match those in libjpeg + m[2] = 1.402f; + m[5] = 1; m[6] = -0.34414f; m[7] = -0.71414f; + m[10] = 1; m[11] = 1.772f; m[12] = 0; + } + + @Override + public boolean equals(Object obj) { + // if (obj == this) return true; -- NaN value would mean matrix != itself + if (!(obj instanceof ColorMatrix)) { + return false; + } + + // we don't use Arrays.equals(), since that considers NaN == NaN + final float[] other = ((ColorMatrix) obj).mArray; + for (int i = 0; i < 20; i++) { + if (other[i] != mArray[i]) { + return false; + } + } + return true; + } +} diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp index a114e2f42157..e1f5abd786bf 100644 --- a/libs/hwui/apex/jni_runtime.cpp +++ b/libs/hwui/apex/jni_runtime.cpp @@ -16,14 +16,14 @@ #include "android/graphics/jni_runtime.h" -#include <android/log.h> -#include <nativehelper/JNIHelp.h> -#include <sys/cdefs.h> - #include <EGL/egl.h> #include <GraphicsJNI.h> #include <Properties.h> #include <SkGraphics.h> +#include <android/log.h> +#include <nativehelper/JNIHelp.h> +#include <sys/cdefs.h> +#include <vulkan/vulkan.h> #undef LOG_TAG #define LOG_TAG "AndroidGraphicsJNI" @@ -43,6 +43,7 @@ extern int register_android_graphics_Movie(JNIEnv* env); extern int register_android_graphics_NinePatch(JNIEnv*); extern int register_android_graphics_PathEffect(JNIEnv* env); extern int register_android_graphics_Shader(JNIEnv* env); +extern int register_android_graphics_RenderEffect(JNIEnv* env); extern int register_android_graphics_Typeface(JNIEnv* env); extern int register_android_graphics_YuvImage(JNIEnv* env); @@ -61,6 +62,7 @@ extern int register_android_graphics_Path(JNIEnv* env); extern int register_android_graphics_PathMeasure(JNIEnv* env); extern int register_android_graphics_Picture(JNIEnv*); extern int register_android_graphics_Region(JNIEnv* env); +extern int register_android_graphics_TextureLayer(JNIEnv* env); extern int register_android_graphics_animation_NativeInterpolatorFactory(JNIEnv* env); extern int register_android_graphics_animation_RenderNodeAnimator(JNIEnv* env); extern int register_android_graphics_drawable_AnimatedVectorDrawable(JNIEnv* env); @@ -72,11 +74,11 @@ extern int register_android_graphics_pdf_PdfEditor(JNIEnv* env); extern int register_android_graphics_pdf_PdfRenderer(JNIEnv* env); extern int register_android_graphics_text_MeasuredText(JNIEnv* env); extern int register_android_graphics_text_LineBreaker(JNIEnv *env); +extern int register_android_graphics_text_TextShaper(JNIEnv *env); extern int register_android_util_PathParser(JNIEnv* env); extern int register_android_view_DisplayListCanvas(JNIEnv* env); extern int register_android_view_RenderNode(JNIEnv* env); -extern int register_android_view_TextureLayer(JNIEnv* env); extern int register_android_view_ThreadedRenderer(JNIEnv* env); #ifdef NDEBUG @@ -123,6 +125,8 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_graphics_Picture), REG_JNI(register_android_graphics_Region), REG_JNI(register_android_graphics_Shader), + REG_JNI(register_android_graphics_RenderEffect), + REG_JNI(register_android_graphics_TextureLayer), REG_JNI(register_android_graphics_Typeface), REG_JNI(register_android_graphics_YuvImage), REG_JNI(register_android_graphics_animation_NativeInterpolatorFactory), @@ -136,11 +140,11 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_graphics_pdf_PdfRenderer), REG_JNI(register_android_graphics_text_MeasuredText), REG_JNI(register_android_graphics_text_LineBreaker), + REG_JNI(register_android_graphics_text_TextShaper), REG_JNI(register_android_util_PathParser), REG_JNI(register_android_view_RenderNode), REG_JNI(register_android_view_DisplayListCanvas), - REG_JNI(register_android_view_TextureLayer), REG_JNI(register_android_view_ThreadedRenderer), }; @@ -172,6 +176,11 @@ using android::uirenderer::RenderPipelineType; void zygote_preload_graphics() { if (Properties::peekRenderPipelineType() == RenderPipelineType::SkiaGL) { + // Preload GL driver if HWUI renders with GL backend. eglGetDisplay(EGL_DEFAULT_DISPLAY); + } else { + // Preload Vulkan driver if HWUI renders with Vulkan backend. + uint32_t apiVersion; + vkEnumerateInstanceVersion(&apiVersion); } -}
\ No newline at end of file +} diff --git a/libs/hwui/api/current.txt b/libs/hwui/api/current.txt new file mode 100644 index 000000000000..c396a2032eed --- /dev/null +++ b/libs/hwui/api/current.txt @@ -0,0 +1,23 @@ +// Signature format: 2.0 +package android.graphics { + + public class ColorMatrix { + ctor public ColorMatrix(); + ctor public ColorMatrix(float[]); + ctor public ColorMatrix(android.graphics.ColorMatrix); + method public final float[] getArray(); + method public void postConcat(android.graphics.ColorMatrix); + method public void preConcat(android.graphics.ColorMatrix); + method public void reset(); + method public void set(android.graphics.ColorMatrix); + method public void set(float[]); + method public void setConcat(android.graphics.ColorMatrix, android.graphics.ColorMatrix); + method public void setRGB2YUV(); + method public void setRotate(int, float); + method public void setSaturation(float); + method public void setScale(float, float, float, float); + method public void setYUV2RGB(); + } + +} + diff --git a/libs/hwui/api/module-lib-current.txt b/libs/hwui/api/module-lib-current.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/libs/hwui/api/module-lib-current.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/libs/hwui/api/module-lib-removed.txt b/libs/hwui/api/module-lib-removed.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/libs/hwui/api/module-lib-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/libs/hwui/api/removed.txt b/libs/hwui/api/removed.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/libs/hwui/api/removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/libs/hwui/api/system-current.txt b/libs/hwui/api/system-current.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/libs/hwui/api/system-current.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/libs/hwui/api/system-removed.txt b/libs/hwui/api/system-removed.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/libs/hwui/api/system-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/libs/hwui/hwui/AnimatedImageDrawable.h b/libs/hwui/hwui/AnimatedImageDrawable.h index f0aa35acf71b..f81a5a40b44e 100644 --- a/libs/hwui/hwui/AnimatedImageDrawable.h +++ b/libs/hwui/hwui/AnimatedImageDrawable.h @@ -44,7 +44,7 @@ public: * This class can be drawn into Canvas.h and maintains the state needed to drive * the animation from the RenderThread. */ -class ANDROID_API AnimatedImageDrawable : public SkDrawable { +class AnimatedImageDrawable : public SkDrawable { public: // bytesUsed includes the approximate sizes of the SkAnimatedImage and the SkPictures in the // Snapshots. diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp index 60ef4371d38d..1a89cfd5d0ad 100644 --- a/libs/hwui/hwui/Bitmap.cpp +++ b/libs/hwui/hwui/Bitmap.cpp @@ -33,7 +33,6 @@ #ifndef _WIN32 #include <binder/IServiceManager.h> #endif -#include <ui/PixelFormat.h> #include <SkCanvas.h> #include <SkImagePriv.h> @@ -132,15 +131,8 @@ sk_sp<Bitmap> Bitmap::allocateHeapBitmap(size_t size, const SkImageInfo& info, s return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes)); } -void FreePixelRef(void* addr, void* context) { - auto pixelRef = (SkPixelRef*)context; - pixelRef->unref(); -} - sk_sp<Bitmap> Bitmap::createFrom(const SkImageInfo& info, SkPixelRef& pixelRef) { - pixelRef.ref(); - return sk_sp<Bitmap>(new Bitmap((void*)pixelRef.pixels(), (void*)&pixelRef, FreePixelRef, info, - pixelRef.rowBytes())); + return sk_sp<Bitmap>(new Bitmap(pixelRef, info)); } @@ -230,14 +222,12 @@ Bitmap::Bitmap(void* address, size_t size, const SkImageInfo& info, size_t rowBy mPixelStorage.heap.size = size; } -Bitmap::Bitmap(void* address, void* context, FreeFunc freeFunc, const SkImageInfo& info, - size_t rowBytes) - : SkPixelRef(info.width(), info.height(), address, rowBytes) +Bitmap::Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info) + : SkPixelRef(info.width(), info.height(), pixelRef.pixels(), pixelRef.rowBytes()) , mInfo(validateAlpha(info)) - , mPixelStorageType(PixelStorageType::External) { - mPixelStorage.external.address = address; - mPixelStorage.external.context = context; - mPixelStorage.external.freeFunc = freeFunc; + , mPixelStorageType(PixelStorageType::WrappedPixelRef) { + pixelRef.ref(); + mPixelStorage.wrapped.pixelRef = &pixelRef; } Bitmap::Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, size_t rowBytes) @@ -266,9 +256,8 @@ Bitmap::Bitmap(AHardwareBuffer* buffer, const SkImageInfo& info, size_t rowBytes Bitmap::~Bitmap() { switch (mPixelStorageType) { - case PixelStorageType::External: - mPixelStorage.external.freeFunc(mPixelStorage.external.address, - mPixelStorage.external.context); + case PixelStorageType::WrappedPixelRef: + mPixelStorage.wrapped.pixelRef->unref(); break; case PixelStorageType::Ashmem: #ifndef _WIN32 // ashmem not implemented on Windows @@ -300,19 +289,6 @@ void Bitmap::setHasHardwareMipMap(bool hasMipMap) { mHasHardwareMipMap = hasMipMap; } -void* Bitmap::getStorage() const { - switch (mPixelStorageType) { - case PixelStorageType::External: - return mPixelStorage.external.address; - case PixelStorageType::Ashmem: - return mPixelStorage.ashmem.address; - case PixelStorageType::Heap: - return mPixelStorage.heap.address; - case PixelStorageType::Hardware: - return nullptr; - } -} - int Bitmap::getAshmemFd() const { switch (mPixelStorageType) { case PixelStorageType::Ashmem: diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h index b8b59947a57b..6ece7ef9f329 100644 --- a/libs/hwui/hwui/Bitmap.h +++ b/libs/hwui/hwui/Bitmap.h @@ -32,7 +32,7 @@ class SkWStream; namespace android { enum class PixelStorageType { - External, + WrappedPixelRef, Heap, Ashmem, Hardware, @@ -56,7 +56,7 @@ class PixelStorage; typedef void (*FreeFunc)(void* addr, void* context); -class ANDROID_API Bitmap : public SkPixelRef { +class Bitmap : public SkPixelRef { public: /* The allocate factories not only construct the Bitmap object but also allocate the * backing store whose type is determined by the specific method that is called. @@ -71,6 +71,7 @@ public: static sk_sp<Bitmap> allocateHardwareBitmap(const SkBitmap& bitmap); static sk_sp<Bitmap> allocateHeapBitmap(SkBitmap* bitmap); static sk_sp<Bitmap> allocateHeapBitmap(const SkImageInfo& info); + static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& i, size_t rowBytes); /* The createFrom factories construct a new Bitmap object by wrapping the already allocated * memory that is provided as an input param. @@ -160,11 +161,9 @@ public: int32_t quality, SkWStream* stream); private: static sk_sp<Bitmap> allocateAshmemBitmap(size_t size, const SkImageInfo& i, size_t rowBytes); - static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& i, size_t rowBytes); Bitmap(void* address, size_t allocSize, const SkImageInfo& info, size_t rowBytes); - Bitmap(void* address, void* context, FreeFunc freeFunc, const SkImageInfo& info, - size_t rowBytes); + Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info); Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, size_t rowBytes); #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration Bitmap(AHardwareBuffer* buffer, const SkImageInfo& info, size_t rowBytes, @@ -178,7 +177,6 @@ private: #endif virtual ~Bitmap(); - void* getStorage() const; SkImageInfo mInfo; @@ -191,10 +189,8 @@ private: union { struct { - void* address; - void* context; - FreeFunc freeFunc; - } external; + SkPixelRef* pixelRef; + } wrapped; struct { void* address; int fd; diff --git a/libs/hwui/hwui/Canvas.cpp b/libs/hwui/hwui/Canvas.cpp index c138a32eacc2..146bf283c58a 100644 --- a/libs/hwui/hwui/Canvas.cpp +++ b/libs/hwui/hwui/Canvas.cpp @@ -84,13 +84,12 @@ static void simplifyPaint(int color, Paint* paint) { class DrawTextFunctor { public: DrawTextFunctor(const minikin::Layout& layout, Canvas* canvas, const Paint& paint, float x, - float y, minikin::MinikinRect& bounds, float totalAdvance) + float y, float totalAdvance) : layout(layout) , canvas(canvas) , paint(paint) , x(x) , y(y) - , bounds(bounds) , totalAdvance(totalAdvance) {} void operator()(size_t start, size_t end) { @@ -114,19 +113,16 @@ public: Paint outlinePaint(paint); simplifyPaint(darken ? SK_ColorWHITE : SK_ColorBLACK, &outlinePaint); outlinePaint.setStyle(SkPaint::kStrokeAndFill_Style); - canvas->drawGlyphs(glyphFunc, glyphCount, outlinePaint, x, y, bounds.mLeft, bounds.mTop, - bounds.mRight, bounds.mBottom, totalAdvance); + canvas->drawGlyphs(glyphFunc, glyphCount, outlinePaint, x, y, totalAdvance); // inner Paint innerPaint(paint); simplifyPaint(darken ? SK_ColorBLACK : SK_ColorWHITE, &innerPaint); innerPaint.setStyle(SkPaint::kFill_Style); - canvas->drawGlyphs(glyphFunc, glyphCount, innerPaint, x, y, bounds.mLeft, bounds.mTop, - bounds.mRight, bounds.mBottom, totalAdvance); + canvas->drawGlyphs(glyphFunc, glyphCount, innerPaint, x, y, totalAdvance); } else { // standard draw path - canvas->drawGlyphs(glyphFunc, glyphCount, paint, x, y, bounds.mLeft, bounds.mTop, - bounds.mRight, bounds.mBottom, totalAdvance); + canvas->drawGlyphs(glyphFunc, glyphCount, paint, x, y, totalAdvance); } } @@ -136,10 +132,29 @@ private: const Paint& paint; float x; float y; - minikin::MinikinRect& bounds; float totalAdvance; }; +void Canvas::drawGlyphs(const minikin::Font& font, const int* glyphIds, const float* positions, + int glyphCount, const Paint& paint) { + // Minikin modify skFont for auto-fakebold/auto-fakeitalic. + Paint copied(paint); + + auto glyphFunc = [&](uint16_t* outGlyphIds, float* outPositions) { + for (uint32_t i = 0; i < glyphCount; ++i) { + outGlyphIds[i] = static_cast<uint16_t>(glyphIds[i]); + } + memcpy(outPositions, positions, sizeof(float) * 2 * glyphCount); + }; + + const minikin::MinikinFont* minikinFont = font.typeface().get(); + SkFont* skfont = &copied.getSkFont(); + MinikinFontSkia::populateSkFont(skfont, minikinFont, minikin::FontFakery()); + + // total advance is used for drawing underline. We do not support underlyine by glyph drawing. + drawGlyphs(glyphFunc, glyphCount, copied, 0 /* x */, 0 /* y */, 0 /* total Advance */); +} + void Canvas::drawText(const uint16_t* text, int textSize, int start, int count, int contextStart, int contextCount, float x, float y, minikin::Bidi bidiFlags, const Paint& origPaint, const Typeface* typeface, minikin::MeasuredText* mt) { @@ -156,15 +171,12 @@ void Canvas::drawText(const uint16_t* text, int textSize, int start, int count, x += MinikinUtils::xOffsetForTextAlign(&paint, layout); - minikin::MinikinRect bounds; - layout.getBounds(&bounds); - // Set align to left for drawing, as we don't want individual // glyphs centered or right-aligned; the offset above takes // care of all alignment. paint.setTextAlign(Paint::kLeft_Align); - DrawTextFunctor f(layout, this, paint, x, y, bounds, layout.getAdvance()); + DrawTextFunctor f(layout, this, paint, x, y, layout.getAdvance()); MinikinUtils::forFontRun(layout, &paint, f); } diff --git a/libs/hwui/hwui/Canvas.h b/libs/hwui/hwui/Canvas.h index 27dfed305a94..772b7a28ef04 100644 --- a/libs/hwui/hwui/Canvas.h +++ b/libs/hwui/hwui/Canvas.h @@ -20,7 +20,6 @@ #include <utils/Functor.h> #include <androidfw/ResourceTypes.h> -#include "GlFunctorLifecycleListener.h" #include "Properties.h" #include "utils/Macros.h" @@ -33,6 +32,7 @@ class SkCanvasState; class SkVertices; namespace minikin { +class Font; class Layout; class MeasuredText; enum class Bidi : uint8_t; @@ -144,7 +144,7 @@ public: virtual void resetRecording(int width, int height, uirenderer::RenderNode* renderNode = nullptr) = 0; virtual uirenderer::DisplayList* finishRecording() = 0; - virtual void insertReorderBarrier(bool enableReorder) = 0; + virtual void enableZ(bool enableZ) = 0; bool isHighContrastText() const { return uirenderer::Properties::enableHighContrastText; } @@ -162,8 +162,7 @@ public: virtual void drawLayer(uirenderer::DeferredLayerUpdater* layerHandle) = 0; virtual void drawRenderNode(uirenderer::RenderNode* renderNode) = 0; - virtual void callDrawGLFunction(Functor* functor, - uirenderer::GlFunctorLifecycleListener* listener) = 0; + virtual void drawWebViewFunctor(int /*functor*/) { LOG_ALWAYS_FATAL("Not supported"); } @@ -257,6 +256,9 @@ public: */ virtual void drawVectorDrawable(VectorDrawableRoot* tree) = 0; + void drawGlyphs(const minikin::Font& font, const int* glyphIds, const float* positions, + int glyphCount, const Paint& paint); + /** * Converts utf16 text to glyphs, calculating position and boundary, * and delegating the final draw to virtual drawGlyphs method. @@ -290,8 +292,7 @@ protected: * totalAdvance: used to define width of text decorations (underlines, strikethroughs). */ virtual void drawGlyphs(ReadGlyphFunc glyphFunc, int count, const Paint& paint, float x, - float y, float boundsLeft, float boundsTop, float boundsRight, - float boundsBottom, float totalAdvance) = 0; + float y,float totalAdvance) = 0; virtual void drawLayoutOnPath(const minikin::Layout& layout, float hOffset, float vOffset, const Paint& paint, const SkPath& path, size_t start, size_t end) = 0; diff --git a/libs/hwui/hwui/MinikinSkia.cpp b/libs/hwui/hwui/MinikinSkia.cpp index 6a12a203b9f8..0e338f35b8e7 100644 --- a/libs/hwui/hwui/MinikinSkia.cpp +++ b/libs/hwui/hwui/MinikinSkia.cpp @@ -33,8 +33,7 @@ namespace android { MinikinFontSkia::MinikinFontSkia(sk_sp<SkTypeface> typeface, const void* fontData, size_t fontSize, std::string_view filePath, int ttcIndex, const std::vector<minikin::FontVariation>& axes) - : minikin::MinikinFont(typeface->uniqueID()) - , mTypeface(std::move(typeface)) + : mTypeface(std::move(typeface)) , mFontData(fontData) , mFontSize(fontSize) , mTtcIndex(ttcIndex) @@ -125,22 +124,22 @@ const std::vector<minikin::FontVariation>& MinikinFontSkia::GetAxes() const { std::shared_ptr<minikin::MinikinFont> MinikinFontSkia::createFontWithVariation( const std::vector<minikin::FontVariation>& variations) const { - SkFontArguments params; + SkFontArguments args; int ttcIndex; std::unique_ptr<SkStreamAsset> stream(mTypeface->openStream(&ttcIndex)); LOG_ALWAYS_FATAL_IF(stream == nullptr, "openStream failed"); - params.setCollectionIndex(ttcIndex); - std::vector<SkFontArguments::Axis> skAxes; - skAxes.resize(variations.size()); + args.setCollectionIndex(ttcIndex); + std::vector<SkFontArguments::VariationPosition::Coordinate> skVariation; + skVariation.resize(variations.size()); for (size_t i = 0; i < variations.size(); i++) { - skAxes[i].fTag = variations[i].axisTag; - skAxes[i].fStyleValue = SkFloatToScalar(variations[i].value); + skVariation[i].axis = variations[i].axisTag; + skVariation[i].value = SkFloatToScalar(variations[i].value); } - params.setAxes(skAxes.data(), skAxes.size()); + args.setVariationDesignPosition({skVariation.data(), static_cast<int>(skVariation.size())}); sk_sp<SkFontMgr> fm(SkFontMgr::RefDefault()); - sk_sp<SkTypeface> face(fm->makeFromStream(std::move(stream), params)); + sk_sp<SkTypeface> face(fm->makeFromStream(std::move(stream), args)); return std::make_shared<MinikinFontSkia>(std::move(face), mFontData, mFontSize, mFilePath, ttcIndex, variations); diff --git a/libs/hwui/hwui/MinikinSkia.h b/libs/hwui/hwui/MinikinSkia.h index 298967689cd9..77a21428f36a 100644 --- a/libs/hwui/hwui/MinikinSkia.h +++ b/libs/hwui/hwui/MinikinSkia.h @@ -49,6 +49,8 @@ public: void GetFontExtent(minikin::MinikinExtent* extent, const minikin::MinikinPaint& paint, const minikin::FontFakery& fakery) const override; + const std::string& GetFontPath() const override { return mFilePath; } + SkTypeface* GetSkTypeface() const; sk_sp<SkTypeface> RefSkTypeface() const; diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp index 5f6b53ac767f..b8029087cb4f 100644 --- a/libs/hwui/hwui/MinikinUtils.cpp +++ b/libs/hwui/hwui/MinikinUtils.cpp @@ -21,6 +21,7 @@ #include <log/log.h> #include <minikin/MeasuredText.h> +#include <minikin/Measurement.h> #include "Paint.h" #include "SkPathMeasure.h" #include "Typeface.h" @@ -69,6 +70,18 @@ minikin::Layout MinikinUtils::doLayout(const Paint* paint, minikin::Bidi bidiFla } } +void MinikinUtils::getBounds(const Paint* paint, minikin::Bidi bidiFlags, const Typeface* typeface, + const uint16_t* buf, size_t bufSize, minikin::MinikinRect* out) { + minikin::MinikinPaint minikinPaint = prepareMinikinPaint(paint, typeface); + + const minikin::U16StringPiece textBuf(buf, bufSize); + const minikin::StartHyphenEdit startHyphen = paint->getStartHyphenEdit(); + const minikin::EndHyphenEdit endHyphen = paint->getEndHyphenEdit(); + + minikin::getBounds(textBuf, minikin::Range(0, textBuf.size()), bidiFlags, minikinPaint, + startHyphen, endHyphen, out); +} + float MinikinUtils::measureText(const Paint* paint, minikin::Bidi bidiFlags, const Typeface* typeface, const uint16_t* buf, size_t start, size_t count, size_t bufSize, float* advances) { diff --git a/libs/hwui/hwui/MinikinUtils.h b/libs/hwui/hwui/MinikinUtils.h index cbf409504675..a15803ad2dca 100644 --- a/libs/hwui/hwui/MinikinUtils.h +++ b/libs/hwui/hwui/MinikinUtils.h @@ -39,37 +39,40 @@ namespace android { class MinikinUtils { public: - ANDROID_API static minikin::MinikinPaint prepareMinikinPaint(const Paint* paint, + static minikin::MinikinPaint prepareMinikinPaint(const Paint* paint, const Typeface* typeface); - ANDROID_API static minikin::Layout doLayout(const Paint* paint, minikin::Bidi bidiFlags, + static minikin::Layout doLayout(const Paint* paint, minikin::Bidi bidiFlags, const Typeface* typeface, const uint16_t* buf, size_t bufSize, size_t start, size_t count, size_t contextStart, size_t contextCount, minikin::MeasuredText* mt); - ANDROID_API static float measureText(const Paint* paint, minikin::Bidi bidiFlags, + static void getBounds(const Paint* paint, minikin::Bidi bidiFlags, const Typeface* typeface, + const uint16_t* buf, size_t bufSize, minikin::MinikinRect* out); + + static float measureText(const Paint* paint, minikin::Bidi bidiFlags, const Typeface* typeface, const uint16_t* buf, size_t start, size_t count, size_t bufSize, float* advances); - ANDROID_API static bool hasVariationSelector(const Typeface* typeface, uint32_t codepoint, + static bool hasVariationSelector(const Typeface* typeface, uint32_t codepoint, uint32_t vs); - ANDROID_API static float xOffsetForTextAlign(Paint* paint, const minikin::Layout& layout); + static float xOffsetForTextAlign(Paint* paint, const minikin::Layout& layout); - ANDROID_API static float hOffsetForTextAlign(Paint* paint, const minikin::Layout& layout, + static float hOffsetForTextAlign(Paint* paint, const minikin::Layout& layout, const SkPath& path); // f is a functor of type void f(size_t start, size_t end); template <typename F> - ANDROID_API static void forFontRun(const minikin::Layout& layout, Paint* paint, F& f) { + static void forFontRun(const minikin::Layout& layout, Paint* paint, F& f) { float saveSkewX = paint->getSkFont().getSkewX(); bool savefakeBold = paint->getSkFont().isEmbolden(); const minikin::MinikinFont* curFont = nullptr; size_t start = 0; size_t nGlyphs = layout.nGlyphs(); for (size_t i = 0; i < nGlyphs; i++) { - const minikin::MinikinFont* nextFont = layout.getFont(i); + const minikin::MinikinFont* nextFont = layout.getFont(i)->typeface().get(); if (i > 0 && nextFont != curFont) { SkFont* skfont = &paint->getSkFont(); MinikinFontSkia::populateSkFont(skfont, curFont, layout.getFakery(start)); diff --git a/libs/hwui/hwui/Paint.h b/libs/hwui/hwui/Paint.h index 281ecd27d780..e75e9e7c6933 100644 --- a/libs/hwui/hwui/Paint.h +++ b/libs/hwui/hwui/Paint.h @@ -32,7 +32,7 @@ namespace android { -class ANDROID_API Paint : public SkPaint { +class Paint : public SkPaint { public: // Default values for underlined and strikethrough text, // as defined by Skia in SkTextFormatParams.h. diff --git a/libs/hwui/hwui/Typeface.cpp b/libs/hwui/hwui/Typeface.cpp index ccc328c702db..03f1d62625f1 100644 --- a/libs/hwui/hwui/Typeface.cpp +++ b/libs/hwui/hwui/Typeface.cpp @@ -188,7 +188,7 @@ void Typeface::setRobotoTypefaceForTest() { std::shared_ptr<minikin::MinikinFont> font = std::make_shared<MinikinFontSkia>( std::move(typeface), data, st.st_size, kRobotoFont, 0, std::vector<minikin::FontVariation>()); - std::vector<minikin::Font> fonts; + std::vector<std::shared_ptr<minikin::Font>> fonts; fonts.push_back(minikin::Font::Builder(font).build()); std::shared_ptr<minikin::FontCollection> collection = std::make_shared<minikin::FontCollection>( diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp index c0663a9bc699..eb9885a4436a 100755 --- a/libs/hwui/jni/Bitmap.cpp +++ b/libs/hwui/jni/Bitmap.cpp @@ -19,12 +19,19 @@ #include <utils/Color.h> #ifdef __ANDROID__ // Layoutlib does not support graphic buffer, parcel or render thread +#include <android-base/unique_fd.h> +#include <android/binder_parcel.h> +#include <android/binder_parcel_jni.h> +#include <android/binder_parcel_platform.h> +#include <android/binder_parcel_utils.h> #include <private/android/AHardwareBufferHelpers.h> -#include <binder/Parcel.h> +#include <cutils/ashmem.h> #include <dlfcn.h> #include <renderthread/RenderProxy.h> +#include <sys/mman.h> #endif +#include <inttypes.h> #include <string.h> #include <memory> #include <string> @@ -567,152 +574,296 @@ static void Bitmap_setHasMipMap(JNIEnv* env, jobject, jlong bitmapHandle, /////////////////////////////////////////////////////////////////////////////// +// TODO: Move somewhere else #ifdef __ANDROID__ // Layoutlib does not support parcel -static struct parcel_offsets_t -{ - jclass clazz; - jfieldID mNativePtr; -} gParcelOffsets; - -static Parcel* parcelForJavaObject(JNIEnv* env, jobject obj) { - if (obj) { - Parcel* p = (Parcel*)env->GetLongField(obj, gParcelOffsets.mNativePtr); - if (p != NULL) { - return p; + +class ScopedParcel { +public: + explicit ScopedParcel(JNIEnv* env, jobject parcel) { + mParcel = AParcel_fromJavaParcel(env, parcel); + } + + ~ScopedParcel() { AParcel_delete(mParcel); } + + int32_t readInt32() { + int32_t temp = 0; + // TODO: This behavior-matches what android::Parcel does + // but this should probably be better + if (AParcel_readInt32(mParcel, &temp) != STATUS_OK) { + temp = 0; } - jniThrowException(env, "java/lang/IllegalStateException", "Parcel has been finalized!"); + return temp; + } + + uint32_t readUint32() { + uint32_t temp = 0; + // TODO: This behavior-matches what android::Parcel does + // but this should probably be better + if (AParcel_readUint32(mParcel, &temp) != STATUS_OK) { + temp = 0; + } + return temp; + } + + void writeInt32(int32_t value) { AParcel_writeInt32(mParcel, value); } + + void writeUint32(uint32_t value) { AParcel_writeUint32(mParcel, value); } + + bool allowFds() const { return AParcel_getAllowFds(mParcel); } + + std::optional<sk_sp<SkData>> readData() { + struct Data { + void* ptr = nullptr; + size_t size = 0; + } data; + auto error = AParcel_readByteArray(mParcel, &data, + [](void* arrayData, int32_t length, + int8_t** outBuffer) -> bool { + Data* data = reinterpret_cast<Data*>(arrayData); + if (length > 0) { + data->ptr = sk_malloc_canfail(length); + if (!data->ptr) { + return false; + } + *outBuffer = + reinterpret_cast<int8_t*>(data->ptr); + data->size = length; + } + return true; + }); + if (error != STATUS_OK || data.size <= 0) { + sk_free(data.ptr); + return std::nullopt; + } else { + return SkData::MakeFromMalloc(data.ptr, data.size); + } + } + + void writeData(const std::optional<sk_sp<SkData>>& optData) { + if (optData) { + const auto& data = *optData; + AParcel_writeByteArray(mParcel, reinterpret_cast<const int8_t*>(data->data()), + data->size()); + } else { + AParcel_writeByteArray(mParcel, nullptr, -1); + } + } + + AParcel* get() { return mParcel; } + +private: + AParcel* mParcel; +}; + +enum class BlobType : int32_t { + IN_PLACE, + ASHMEM, +}; + +#define ON_ERROR_RETURN(X) \ + if ((error = (X)) != STATUS_OK) return error + +template <typename T, typename U> +static binder_status_t readBlob(AParcel* parcel, T inPlaceCallback, U ashmemCallback) { + binder_status_t error = STATUS_OK; + BlobType type; + static_assert(sizeof(BlobType) == sizeof(int32_t)); + ON_ERROR_RETURN(AParcel_readInt32(parcel, (int32_t*)&type)); + if (type == BlobType::IN_PLACE) { + struct Data { + std::unique_ptr<int8_t[]> ptr = nullptr; + int32_t size = 0; + } data; + ON_ERROR_RETURN( + AParcel_readByteArray(parcel, &data, + [](void* arrayData, int32_t length, int8_t** outBuffer) { + Data* data = reinterpret_cast<Data*>(arrayData); + if (length > 0) { + data->ptr = std::make_unique<int8_t[]>(length); + data->size = length; + *outBuffer = data->ptr.get(); + } + return data->ptr != nullptr; + })); + inPlaceCallback(std::move(data.ptr), data.size); + return STATUS_OK; + } else if (type == BlobType::ASHMEM) { + int rawFd = -1; + int32_t size = 0; + ON_ERROR_RETURN(AParcel_readInt32(parcel, &size)); + ON_ERROR_RETURN(AParcel_readParcelFileDescriptor(parcel, &rawFd)); + android::base::unique_fd fd(rawFd); + ashmemCallback(std::move(fd), size); + return STATUS_OK; + } else { + // Although the above if/else was "exhaustive" guard against unknown types + return STATUS_UNKNOWN_ERROR; } - return NULL; } -#endif + +static constexpr size_t BLOB_INPLACE_LIMIT = 12 * 1024; +// Fail fast if we can't use ashmem and the size exceeds this limit - the binder transaction +// wouldn't go through, anyway +// TODO: Can we get this from somewhere? +static constexpr size_t BLOB_MAX_INPLACE_LIMIT = 1 * 1024 * 1024; +static constexpr bool shouldUseAshmem(AParcel* parcel, int32_t size) { + return size > BLOB_INPLACE_LIMIT && AParcel_getAllowFds(parcel); +} + +static binder_status_t writeBlobFromFd(AParcel* parcel, int32_t size, int fd) { + binder_status_t error = STATUS_OK; + ON_ERROR_RETURN(AParcel_writeInt32(parcel, static_cast<int32_t>(BlobType::ASHMEM))); + ON_ERROR_RETURN(AParcel_writeInt32(parcel, size)); + ON_ERROR_RETURN(AParcel_writeParcelFileDescriptor(parcel, fd)); + return STATUS_OK; +} + +static binder_status_t writeBlob(AParcel* parcel, const int32_t size, const void* data, bool immutable) { + if (size <= 0 || data == nullptr) { + return STATUS_NOT_ENOUGH_DATA; + } + binder_status_t error = STATUS_OK; + if (shouldUseAshmem(parcel, size)) { + // Create new ashmem region with read/write priv + base::unique_fd fd(ashmem_create_region("bitmap", size)); + if (fd.get() < 0) { + return STATUS_NO_MEMORY; + } + + { + void* dest = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd.get(), 0); + if (dest == MAP_FAILED) { + return STATUS_NO_MEMORY; + } + memcpy(dest, data, size); + munmap(dest, size); + } + + if (immutable && ashmem_set_prot_region(fd.get(), PROT_READ) < 0) { + return STATUS_UNKNOWN_ERROR; + } + // Workaround b/149851140 in AParcel_writeParcelFileDescriptor + int rawFd = fd.release(); + error = writeBlobFromFd(parcel, size, rawFd); + close(rawFd); + return error; + } else { + if (size > BLOB_MAX_INPLACE_LIMIT) { + return STATUS_FAILED_TRANSACTION; + } + ON_ERROR_RETURN(AParcel_writeInt32(parcel, static_cast<int32_t>(BlobType::IN_PLACE))); + ON_ERROR_RETURN(AParcel_writeByteArray(parcel, static_cast<const int8_t*>(data), size)); + return STATUS_OK; + } +} + +#undef ON_ERROR_RETURN + +#endif // __ANDROID__ // Layoutlib does not support parcel // This is the maximum possible size because the SkColorSpace must be // representable (and therefore serializable) using a matrix and numerical // transfer function. If we allow more color space representations in the // framework, we may need to update this maximum size. -static constexpr uint32_t kMaxColorSpaceSerializedBytes = 80; +static constexpr size_t kMaxColorSpaceSerializedBytes = 80; + +static constexpr auto RuntimeException = "java/lang/RuntimeException"; + +static bool validateImageInfo(const SkImageInfo& info, int32_t rowBytes) { + // TODO: Can we avoid making a SkBitmap for this? + return SkBitmap().setInfo(info, rowBytes); +} static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) { #ifdef __ANDROID__ // Layoutlib does not support parcel if (parcel == NULL) { - SkDebugf("-------- unparcel parcel is NULL\n"); + jniThrowNullPointerException(env, "parcel cannot be null"); return NULL; } - android::Parcel* p = parcelForJavaObject(env, parcel); + ScopedParcel p(env, parcel); - const bool isMutable = p->readInt32() != 0; - const SkColorType colorType = (SkColorType)p->readInt32(); - const SkAlphaType alphaType = (SkAlphaType)p->readInt32(); - const uint32_t colorSpaceSize = p->readUint32(); + const bool isMutable = p.readInt32(); + const SkColorType colorType = static_cast<SkColorType>(p.readInt32()); + const SkAlphaType alphaType = static_cast<SkAlphaType>(p.readInt32()); sk_sp<SkColorSpace> colorSpace; - if (colorSpaceSize > 0) { - if (colorSpaceSize > kMaxColorSpaceSerializedBytes) { + const auto optColorSpaceData = p.readData(); + if (optColorSpaceData) { + const auto& colorSpaceData = *optColorSpaceData; + if (colorSpaceData->size() > kMaxColorSpaceSerializedBytes) { ALOGD("Bitmap_createFromParcel: Serialized SkColorSpace is larger than expected: " - "%d bytes\n", colorSpaceSize); + "%zu bytes (max: %zu)\n", + colorSpaceData->size(), kMaxColorSpaceSerializedBytes); } - const void* data = p->readInplace(colorSpaceSize); - if (data) { - colorSpace = SkColorSpace::Deserialize(data, colorSpaceSize); - } else { - ALOGD("Bitmap_createFromParcel: Unable to read serialized SkColorSpace data\n"); - } + colorSpace = SkColorSpace::Deserialize(colorSpaceData->data(), colorSpaceData->size()); } - const int width = p->readInt32(); - const int height = p->readInt32(); - const int rowBytes = p->readInt32(); - const int density = p->readInt32(); + const int32_t width = p.readInt32(); + const int32_t height = p.readInt32(); + const int32_t rowBytes = p.readInt32(); + const int32_t density = p.readInt32(); if (kN32_SkColorType != colorType && kRGBA_F16_SkColorType != colorType && kRGB_565_SkColorType != colorType && kARGB_4444_SkColorType != colorType && kAlpha_8_SkColorType != colorType) { - SkDebugf("Bitmap_createFromParcel unknown colortype: %d\n", colorType); + jniThrowExceptionFmt(env, RuntimeException, + "Bitmap_createFromParcel unknown colortype: %d\n", colorType); return NULL; } - std::unique_ptr<SkBitmap> bitmap(new SkBitmap); - if (!bitmap->setInfo(SkImageInfo::Make(width, height, colorType, alphaType, colorSpace), - rowBytes)) { + auto imageInfo = SkImageInfo::Make(width, height, colorType, alphaType, colorSpace); + size_t allocationSize = 0; + if (!validateImageInfo(imageInfo, rowBytes)) { + jniThrowRuntimeException(env, "Received bad SkImageInfo"); return NULL; } - - // Read the bitmap blob. - size_t size = bitmap->computeByteSize(); - android::Parcel::ReadableBlob blob; - android::status_t status = p->readBlob(size, &blob); - if (status) { - doThrowRE(env, "Could not read bitmap blob."); + if (!Bitmap::computeAllocationSize(rowBytes, height, &allocationSize)) { + jniThrowExceptionFmt(env, RuntimeException, + "Received bad bitmap size: width=%d, height=%d, rowBytes=%d", width, + height, rowBytes); return NULL; } - - // Map the bitmap in place from the ashmem region if possible otherwise copy. sk_sp<Bitmap> nativeBitmap; - // If the blob is mutable we have ownership of the region and can always use it - // If the blob is immutable _and_ we're immutable, we can then still use it - if (blob.fd() >= 0 && (blob.isMutable() || !isMutable)) { -#if DEBUG_PARCEL - ALOGD("Bitmap.createFromParcel: mapped contents of bitmap from %s blob " - "(fds %s)", - blob.isMutable() ? "mutable" : "immutable", - p->allowFds() ? "allowed" : "forbidden"); -#endif - // Dup the file descriptor so we can keep a reference to it after the Parcel - // is disposed. - int dupFd = fcntl(blob.fd(), F_DUPFD_CLOEXEC, 0); - if (dupFd < 0) { - ALOGE("Error allocating dup fd. Error:%d", errno); - blob.release(); - doThrowRE(env, "Could not allocate dup blob fd."); - return NULL; - } - - // Map the pixels in place and take ownership of the ashmem region. We must also respect the - // rowBytes value already set on the bitmap instead of attempting to compute our own. - nativeBitmap = Bitmap::createFrom(bitmap->info(), bitmap->rowBytes(), dupFd, - const_cast<void*>(blob.data()), size, !isMutable); - if (!nativeBitmap) { - close(dupFd); - blob.release(); - doThrowRE(env, "Could not allocate ashmem pixel ref."); - return NULL; - } - - // Clear the blob handle, don't release it. - blob.clear(); - } else { -#if DEBUG_PARCEL - if (blob.fd() >= 0) { - ALOGD("Bitmap.createFromParcel: copied contents of mutable bitmap " - "from immutable blob (fds %s)", - p->allowFds() ? "allowed" : "forbidden"); - } else { - ALOGD("Bitmap.createFromParcel: copied contents from %s blob " - "(fds %s)", - blob.isMutable() ? "mutable" : "immutable", - p->allowFds() ? "allowed" : "forbidden"); - } -#endif - - // Copy the pixels into a new buffer. - nativeBitmap = Bitmap::allocateHeapBitmap(bitmap.get()); - if (!nativeBitmap) { - blob.release(); - doThrowRE(env, "Could not allocate java pixel ref."); - return NULL; - } - memcpy(bitmap->getPixels(), blob.data(), size); - - // Release the blob handle. - blob.release(); + binder_status_t error = readBlob( + p.get(), + // In place callback + [&](std::unique_ptr<int8_t[]> buffer, int32_t size) { + nativeBitmap = Bitmap::allocateHeapBitmap(allocationSize, imageInfo, rowBytes); + if (nativeBitmap) { + memcpy(nativeBitmap->pixels(), buffer.get(), size); + } + }, + // Ashmem callback + [&](android::base::unique_fd fd, int32_t size) { + int flags = PROT_READ; + if (isMutable) { + flags |= PROT_WRITE; + } + void* addr = mmap(nullptr, size, flags, MAP_SHARED, fd.get(), 0); + if (addr == MAP_FAILED) { + const int err = errno; + ALOGW("mmap failed, error %d (%s)", err, strerror(err)); + return; + } + nativeBitmap = + Bitmap::createFrom(imageInfo, rowBytes, fd.release(), addr, size, !isMutable); + }); + if (error != STATUS_OK) { + // TODO: Stringify the error, see signalExceptionForError in android_util_Binder.cpp + jniThrowExceptionFmt(env, RuntimeException, "Failed to read from Parcel, error=%d", error); + return nullptr; + } + if (!nativeBitmap) { + jniThrowRuntimeException(env, "Could not allocate java pixel ref."); + return nullptr; } - return createBitmap(env, nativeBitmap.release(), - getPremulBitmapCreateFlags(isMutable), NULL, NULL, density); + return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable), nullptr, + nullptr, density); #else - doThrowRE(env, "Cannot use parcels outside of Android"); + jniThrowRuntimeException(env, "Cannot use parcels outside of Android"); return NULL; #endif } @@ -725,48 +876,38 @@ static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, return JNI_FALSE; } - android::Parcel* p = parcelForJavaObject(env, parcel); + ScopedParcel p(env, parcel); SkBitmap bitmap; auto bitmapWrapper = reinterpret_cast<BitmapWrapper*>(bitmapHandle); bitmapWrapper->getSkBitmap(&bitmap); - p->writeInt32(!bitmap.isImmutable()); - p->writeInt32(bitmap.colorType()); - p->writeInt32(bitmap.alphaType()); + p.writeInt32(!bitmap.isImmutable()); + p.writeInt32(bitmap.colorType()); + p.writeInt32(bitmap.alphaType()); SkColorSpace* colorSpace = bitmap.colorSpace(); if (colorSpace != nullptr) { - sk_sp<SkData> data = colorSpace->serialize(); - size_t size = data->size(); - p->writeUint32(size); - if (size > 0) { - if (size > kMaxColorSpaceSerializedBytes) { - ALOGD("Bitmap_writeToParcel: Serialized SkColorSpace is larger than expected: " - "%zu bytes\n", size); - } - - p->write(data->data(), size); - } + p.writeData(colorSpace->serialize()); } else { - p->writeUint32(0); + p.writeData(std::nullopt); } - p->writeInt32(bitmap.width()); - p->writeInt32(bitmap.height()); - p->writeInt32(bitmap.rowBytes()); - p->writeInt32(density); + p.writeInt32(bitmap.width()); + p.writeInt32(bitmap.height()); + p.writeInt32(bitmap.rowBytes()); + p.writeInt32(density); // Transfer the underlying ashmem region if we have one and it's immutable. - android::status_t status; + binder_status_t status; int fd = bitmapWrapper->bitmap().getAshmemFd(); - if (fd >= 0 && bitmap.isImmutable() && p->allowFds()) { + if (fd >= 0 && p.allowFds() && bitmap.isImmutable()) { #if DEBUG_PARCEL ALOGD("Bitmap.writeToParcel: transferring immutable bitmap's ashmem fd as " - "immutable blob (fds %s)", - p->allowFds() ? "allowed" : "forbidden"); + "immutable blob (fds %s)", + p.allowFds() ? "allowed" : "forbidden"); #endif - status = p->writeDupImmutableBlobFileDescriptor(fd); - if (status) { + status = writeBlobFromFd(p.get(), bitmapWrapper->bitmap().getAllocationByteCount(), fd); + if (status != STATUS_OK) { doThrowRE(env, "Could not write bitmap blob file descriptor."); return JNI_FALSE; } @@ -776,26 +917,15 @@ static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, // Copy the bitmap to a new blob. #if DEBUG_PARCEL ALOGD("Bitmap.writeToParcel: copying bitmap into new blob (fds %s)", - p->allowFds() ? "allowed" : "forbidden"); + p.allowFds() ? "allowed" : "forbidden"); #endif - const bool mutableCopy = !bitmap.isImmutable(); size_t size = bitmap.computeByteSize(); - android::Parcel::WritableBlob blob; - status = p->writeBlob(size, mutableCopy, &blob); + status = writeBlob(p.get(), size, bitmap.getPixels(), bitmap.isImmutable()); if (status) { doThrowRE(env, "Could not copy bitmap to parcel blob."); return JNI_FALSE; } - - const void* pSrc = bitmap.getPixels(); - if (pSrc == NULL) { - memset(blob.data(), 0, size); - } else { - memcpy(blob.data(), pSrc, size); - } - - blob.release(); return JNI_TRUE; #else doThrowRE(env, "Cannot use parcels outside of Android"); @@ -1074,13 +1204,16 @@ static jobject Bitmap_wrapHardwareBufferBitmap(JNIEnv* env, jobject, jobject har static jobject Bitmap_getHardwareBuffer(JNIEnv* env, jobject, jlong bitmapPtr) { #ifdef __ANDROID__ // Layoutlib does not support graphic buffer LocalScopedBitmap bitmapHandle(bitmapPtr); - LOG_ALWAYS_FATAL_IF(!bitmapHandle->isHardware(), + if (!bitmapHandle->isHardware()) { + jniThrowException(env, "java/lang/IllegalStateException", "Hardware config is only supported config in Bitmap_getHardwareBuffer"); + return nullptr; + } Bitmap& bitmap = bitmapHandle->bitmap(); return AHardwareBuffer_toHardwareBuffer(env, bitmap.hardwareBuffer()); #else - return NULL; + return nullptr; #endif } @@ -1091,6 +1224,14 @@ static jboolean Bitmap_isImmutable(CRITICAL_JNI_PARAMS_COMMA jlong bitmapHandle) return bitmapHolder->bitmap().isImmutable() ? JNI_TRUE : JNI_FALSE; } +static jboolean Bitmap_isBackedByAshmem(CRITICAL_JNI_PARAMS_COMMA jlong bitmapHandle) { + LocalScopedBitmap bitmapHolder(bitmapHandle); + if (!bitmapHolder.valid()) return JNI_FALSE; + + return bitmapHolder->bitmap().pixelStorageType() == PixelStorageType::Ashmem ? JNI_TRUE + : JNI_FALSE; +} + static void Bitmap_setImmutable(JNIEnv* env, jobject, jlong bitmapHandle) { LocalScopedBitmap bitmapHolder(bitmapHandle); if (!bitmapHolder.valid()) return; @@ -1157,12 +1298,11 @@ static const JNINativeMethod gBitmapMethods[] = { { "nativeSetImmutable", "(J)V", (void*)Bitmap_setImmutable}, // ------------ @CriticalNative ---------------- - { "nativeIsImmutable", "(J)Z", (void*)Bitmap_isImmutable} + { "nativeIsImmutable", "(J)Z", (void*)Bitmap_isImmutable}, + { "nativeIsBackedByAshmem", "(J)Z", (void*)Bitmap_isBackedByAshmem} }; -const char* const kParcelPathName = "android/os/Parcel"; - int register_android_graphics_Bitmap(JNIEnv* env) { gBitmap_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/Bitmap")); @@ -1180,9 +1320,6 @@ int register_android_graphics_Bitmap(JNIEnv* env) AHardwareBuffer_toHardwareBuffer = (AHB_to_HB)dlsym(handle_, "AHardwareBuffer_toHardwareBuffer"); LOG_ALWAYS_FATAL_IF(AHardwareBuffer_toHardwareBuffer == nullptr, " Failed to find required symbol AHardwareBuffer_toHardwareBuffer!"); - - gParcelOffsets.clazz = MakeGlobalRefOrDie(env, FindClassOrDie(env, kParcelPathName)); - gParcelOffsets.mNativePtr = GetFieldIDOrDie(env, gParcelOffsets.clazz, "mNativePtr", "J"); #endif return android::RegisterMethodsOrDie(env, "android/graphics/Bitmap", gBitmapMethods, NELEM(gBitmapMethods)); diff --git a/libs/hwui/jni/BitmapFactory.cpp b/libs/hwui/jni/BitmapFactory.cpp index e8e89d81bdb7..7d2583a2ac01 100644 --- a/libs/hwui/jni/BitmapFactory.cpp +++ b/libs/hwui/jni/BitmapFactory.cpp @@ -3,12 +3,11 @@ #include "BitmapFactory.h" #include "CreateJavaOutputStreamAdaptor.h" +#include "FrontBufferedStream.h" #include "GraphicsJNI.h" #include "MimeType.h" #include "NinePatchPeeker.h" #include "SkAndroidCodec.h" -#include "SkBRDAllocator.h" -#include "SkFrontBufferedStream.h" #include "SkMath.h" #include "SkPixelRef.h" #include "SkStream.h" @@ -510,8 +509,8 @@ static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteA std::unique_ptr<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage)); if (stream.get()) { - std::unique_ptr<SkStreamRewindable> bufferedStream( - SkFrontBufferedStream::Make(std::move(stream), SkCodec::MinBufferedBytesNeeded())); + std::unique_ptr<SkStreamRewindable> bufferedStream(skia::FrontBufferedStream::Make( + std::move(stream), SkCodec::MinBufferedBytesNeeded())); SkASSERT(bufferedStream.get() != NULL); bitmap = doDecode(env, std::move(bufferedStream), padding, options, inBitmapHandle, colorSpaceHandle); @@ -565,8 +564,8 @@ static jobject nativeDecodeFileDescriptor(JNIEnv* env, jobject clazz, jobject fi // Use a buffered stream. Although an SkFILEStream can be rewound, this // ensures that SkImageDecoder::Factory never rewinds beyond the // current position of the file descriptor. - std::unique_ptr<SkStreamRewindable> stream(SkFrontBufferedStream::Make(std::move(fileStream), - SkCodec::MinBufferedBytesNeeded())); + std::unique_ptr<SkStreamRewindable> stream(skia::FrontBufferedStream::Make( + std::move(fileStream), SkCodec::MinBufferedBytesNeeded())); return doDecode(env, std::move(stream), padding, bitmapFactoryOptions, inBitmapHandle, colorSpaceHandle); diff --git a/libs/hwui/jni/BitmapRegionDecoder.cpp b/libs/hwui/jni/BitmapRegionDecoder.cpp index 712351382d97..4cc05ef6f13b 100644 --- a/libs/hwui/jni/BitmapRegionDecoder.cpp +++ b/libs/hwui/jni/BitmapRegionDecoder.cpp @@ -22,8 +22,8 @@ #include "GraphicsJNI.h" #include "Utils.h" +#include "BitmapRegionDecoder.h" #include "SkBitmap.h" -#include "SkBitmapRegionDecoder.h" #include "SkCodec.h" #include "SkData.h" #include "SkStream.h" @@ -36,10 +36,8 @@ using namespace android; -static jobject createBitmapRegionDecoder(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream) { - std::unique_ptr<SkBitmapRegionDecoder> brd( - SkBitmapRegionDecoder::Create(stream.release(), - SkBitmapRegionDecoder::kAndroidCodec_Strategy)); +static jobject createBitmapRegionDecoder(JNIEnv* env, sk_sp<SkData> data) { + auto brd = skia::BitmapRegionDecoder::Make(std::move(data)); if (!brd) { doThrowIOE(env, "Image format not supported"); return nullObjectReturn("CreateBitmapRegionDecoder returned null"); @@ -49,21 +47,13 @@ static jobject createBitmapRegionDecoder(JNIEnv* env, std::unique_ptr<SkStreamRe } static jobject nativeNewInstanceFromByteArray(JNIEnv* env, jobject, jbyteArray byteArray, - jint offset, jint length, jboolean isShareable) { - /* If isShareable we could decide to just wrap the java array and - share it, but that means adding a globalref to the java array object - For now we just always copy the array's data if isShareable. - */ + jint offset, jint length) { AutoJavaByteArray ar(env, byteArray); - std::unique_ptr<SkMemoryStream> stream(new SkMemoryStream(ar.ptr() + offset, length, true)); - - // the decoder owns the stream. - jobject brd = createBitmapRegionDecoder(env, std::move(stream)); - return brd; + return createBitmapRegionDecoder(env, SkData::MakeWithCopy(ar.ptr() + offset, length)); } static jobject nativeNewInstanceFromFileDescriptor(JNIEnv* env, jobject clazz, - jobject fileDescriptor, jboolean isShareable) { + jobject fileDescriptor) { NPE_CHECK_RETURN_ZERO(env, fileDescriptor); jint descriptor = jniGetFDFromFileDescriptor(env, fileDescriptor); @@ -74,41 +64,28 @@ static jobject nativeNewInstanceFromFileDescriptor(JNIEnv* env, jobject clazz, return nullObjectReturn("fstat return -1"); } - sk_sp<SkData> data(SkData::MakeFromFD(descriptor)); - std::unique_ptr<SkMemoryStream> stream(new SkMemoryStream(std::move(data))); - - // the decoder owns the stream. - jobject brd = createBitmapRegionDecoder(env, std::move(stream)); - return brd; + return createBitmapRegionDecoder(env, SkData::MakeFromFD(descriptor)); } -static jobject nativeNewInstanceFromStream(JNIEnv* env, jobject clazz, - jobject is, // InputStream - jbyteArray storage, // byte[] - jboolean isShareable) { - jobject brd = NULL; - // for now we don't allow shareable with java inputstreams - std::unique_ptr<SkStreamRewindable> stream(CopyJavaInputStream(env, is, storage)); - - if (stream) { - // the decoder owns the stream. - brd = createBitmapRegionDecoder(env, std::move(stream)); +static jobject nativeNewInstanceFromStream(JNIEnv* env, jobject clazz, jobject is, // InputStream + jbyteArray storage) { // byte[] + jobject brd = nullptr; + sk_sp<SkData> data = CopyJavaInputStream(env, is, storage); + + if (data) { + brd = createBitmapRegionDecoder(env, std::move(data)); } return brd; } -static jobject nativeNewInstanceFromAsset(JNIEnv* env, jobject clazz, - jlong native_asset, // Asset - jboolean isShareable) { +static jobject nativeNewInstanceFromAsset(JNIEnv* env, jobject clazz, jlong native_asset) { Asset* asset = reinterpret_cast<Asset*>(native_asset); - std::unique_ptr<SkMemoryStream> stream(CopyAssetToStream(asset)); - if (NULL == stream) { - return NULL; + sk_sp<SkData> data = CopyAssetToData(asset); + if (!data) { + return nullptr; } - // the decoder owns the stream. - jobject brd = createBitmapRegionDecoder(env, std::move(stream)); - return brd; + return createBitmapRegionDecoder(env, data); } /* @@ -158,7 +135,7 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in recycledBytes = recycledBitmap->getAllocationByteCount(); } - SkBitmapRegionDecoder* brd = reinterpret_cast<SkBitmapRegionDecoder*>(brdHandle); + auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle); SkColorType decodeColorType = brd->computeOutputColorType(colorType); if (decodeColorType == kRGBA_F16_SkColorType && isHardware && !uirenderer::HardwareBitmapUploader::hasFP16Support()) { @@ -166,7 +143,7 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in } // Set up the pixel allocator - SkBRDAllocator* allocator = nullptr; + skia::BRDAllocator* allocator = nullptr; RecyclingClippingPixelAllocator recycleAlloc(recycledBitmap, recycledBytes); HeapAllocator heapAlloc; if (javaBitmap) { @@ -230,20 +207,17 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in } static jint nativeGetHeight(JNIEnv* env, jobject, jlong brdHandle) { - SkBitmapRegionDecoder* brd = - reinterpret_cast<SkBitmapRegionDecoder*>(brdHandle); + auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle); return static_cast<jint>(brd->height()); } static jint nativeGetWidth(JNIEnv* env, jobject, jlong brdHandle) { - SkBitmapRegionDecoder* brd = - reinterpret_cast<SkBitmapRegionDecoder*>(brdHandle); + auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle); return static_cast<jint>(brd->width()); } static void nativeClean(JNIEnv* env, jobject, jlong brdHandle) { - SkBitmapRegionDecoder* brd = - reinterpret_cast<SkBitmapRegionDecoder*>(brdHandle); + auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle); delete brd; } @@ -261,22 +235,22 @@ static const JNINativeMethod gBitmapRegionDecoderMethods[] = { { "nativeClean", "(J)V", (void*)nativeClean}, { "nativeNewInstance", - "([BIIZ)Landroid/graphics/BitmapRegionDecoder;", + "([BII)Landroid/graphics/BitmapRegionDecoder;", (void*)nativeNewInstanceFromByteArray }, { "nativeNewInstance", - "(Ljava/io/InputStream;[BZ)Landroid/graphics/BitmapRegionDecoder;", + "(Ljava/io/InputStream;[B)Landroid/graphics/BitmapRegionDecoder;", (void*)nativeNewInstanceFromStream }, { "nativeNewInstance", - "(Ljava/io/FileDescriptor;Z)Landroid/graphics/BitmapRegionDecoder;", + "(Ljava/io/FileDescriptor;)Landroid/graphics/BitmapRegionDecoder;", (void*)nativeNewInstanceFromFileDescriptor }, { "nativeNewInstance", - "(JZ)Landroid/graphics/BitmapRegionDecoder;", + "(J)Landroid/graphics/BitmapRegionDecoder;", (void*)nativeNewInstanceFromAsset }, }; diff --git a/libs/hwui/jni/CreateJavaOutputStreamAdaptor.cpp b/libs/hwui/jni/CreateJavaOutputStreamAdaptor.cpp index f1c6b29204b2..785a5dc995ab 100644 --- a/libs/hwui/jni/CreateJavaOutputStreamAdaptor.cpp +++ b/libs/hwui/jni/CreateJavaOutputStreamAdaptor.cpp @@ -177,8 +177,12 @@ SkStream* CreateJavaInputStreamAdaptor(JNIEnv* env, jobject stream, jbyteArray s return JavaInputStreamAdaptor::Create(env, stream, storage, swallowExceptions); } -static SkMemoryStream* adaptor_to_mem_stream(SkStream* stream) { - SkASSERT(stream != NULL); +sk_sp<SkData> CopyJavaInputStream(JNIEnv* env, jobject inputStream, jbyteArray storage) { + std::unique_ptr<SkStream> stream(CreateJavaInputStreamAdaptor(env, inputStream, storage)); + if (!stream) { + return nullptr; + } + size_t bufferSize = 4096; size_t streamLen = 0; size_t len; @@ -194,18 +198,7 @@ static SkMemoryStream* adaptor_to_mem_stream(SkStream* stream) { } data = (char*)sk_realloc_throw(data, streamLen); - SkMemoryStream* streamMem = new SkMemoryStream(); - streamMem->setMemoryOwned(data, streamLen); - return streamMem; -} - -SkStreamRewindable* CopyJavaInputStream(JNIEnv* env, jobject stream, - jbyteArray storage) { - std::unique_ptr<SkStream> adaptor(CreateJavaInputStreamAdaptor(env, stream, storage)); - if (NULL == adaptor.get()) { - return NULL; - } - return adaptor_to_mem_stream(adaptor.get()); + return SkData::MakeFromMalloc(data, streamLen); } /////////////////////////////////////////////////////////////////////////////// diff --git a/libs/hwui/jni/CreateJavaOutputStreamAdaptor.h b/libs/hwui/jni/CreateJavaOutputStreamAdaptor.h index 849418da01a1..bae40f1e8d2f 100644 --- a/libs/hwui/jni/CreateJavaOutputStreamAdaptor.h +++ b/libs/hwui/jni/CreateJavaOutputStreamAdaptor.h @@ -2,6 +2,7 @@ #define _ANDROID_GRAPHICS_CREATE_JAVA_OUTPUT_STREAM_ADAPTOR_H_ #include "jni.h" +#include "SkData.h" class SkMemoryStream; class SkStream; @@ -27,15 +28,14 @@ SkStream* CreateJavaInputStreamAdaptor(JNIEnv* env, jobject stream, jbyteArray s bool swallowExceptions = true); /** - * Copy a Java InputStream. The result will be rewindable. + * Copy a Java InputStream to an SkData. * @param env JNIEnv object. * @param stream Pointer to Java InputStream. * @param storage Java byte array for retrieving data from the * Java InputStream. - * @return SkStreamRewindable The data in stream will be copied - * to a new SkStreamRewindable. + * @return SkData containing the stream's data. */ -SkStreamRewindable* CopyJavaInputStream(JNIEnv* env, jobject stream, jbyteArray storage); +sk_sp<SkData> CopyJavaInputStream(JNIEnv* env, jobject stream, jbyteArray storage); SkWStream* CreateJavaOutputStreamAdaptor(JNIEnv* env, jobject stream, jbyteArray storage); diff --git a/libs/hwui/jni/FontFamily.cpp b/libs/hwui/jni/FontFamily.cpp index a2fef1e19328..2e85840cad99 100644 --- a/libs/hwui/jni/FontFamily.cpp +++ b/libs/hwui/jni/FontFamily.cpp @@ -42,7 +42,7 @@ struct NativeFamilyBuilder { : langId(langId), variant(static_cast<minikin::FamilyVariant>(variant)) {} uint32_t langId; minikin::FamilyVariant variant; - std::vector<minikin::Font> fonts; + std::vector<std::shared_ptr<minikin::Font>> fonts; std::vector<minikin::FontVariation> axes; }; @@ -104,21 +104,21 @@ static jlong FontFamily_getFamilyReleaseFunc(CRITICAL_JNI_PARAMS) { static bool addSkTypeface(NativeFamilyBuilder* builder, sk_sp<SkData>&& data, int ttcIndex, jint weight, jint italic) { - FatVector<SkFontArguments::Axis, 2> skiaAxes; + FatVector<SkFontArguments::VariationPosition::Coordinate, 2> skVariation; for (const auto& axis : builder->axes) { - skiaAxes.emplace_back(SkFontArguments::Axis{axis.axisTag, axis.value}); + skVariation.push_back({axis.axisTag, axis.value}); } const size_t fontSize = data->size(); const void* fontPtr = data->data(); std::unique_ptr<SkStreamAsset> fontData(new SkMemoryStream(std::move(data))); - SkFontArguments params; - params.setCollectionIndex(ttcIndex); - params.setAxes(skiaAxes.data(), skiaAxes.size()); + SkFontArguments args; + args.setCollectionIndex(ttcIndex); + args.setVariationDesignPosition({skVariation.data(), static_cast<int>(skVariation.size())}); sk_sp<SkFontMgr> fm(SkFontMgr::RefDefault()); - sk_sp<SkTypeface> face(fm->makeFromStream(std::move(fontData), params)); + sk_sp<SkTypeface> face(fm->makeFromStream(std::move(fontData), args)); if (face == NULL) { ALOGE("addFont failed to create font, invalid request"); builder->axes.clear(); diff --git a/libs/hwui/jni/FontUtils.h b/libs/hwui/jni/FontUtils.h index b36b4e60e33a..ba4e56e4c7f7 100644 --- a/libs/hwui/jni/FontUtils.h +++ b/libs/hwui/jni/FontUtils.h @@ -19,6 +19,7 @@ #include <jni.h> #include <memory> +#include <utility> #include <minikin/Font.h> @@ -34,8 +35,8 @@ struct FontFamilyWrapper { }; struct FontWrapper { - FontWrapper(minikin::Font&& font) : font(std::move(font)) {} - minikin::Font font; + explicit FontWrapper(std::shared_ptr<minikin::Font>&& font) : font(font) {} + std::shared_ptr<minikin::Font> font; }; // Utility wrapper for java.util.List diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp index f76ecb4c9c8a..77f46beb2100 100644 --- a/libs/hwui/jni/Graphics.cpp +++ b/libs/hwui/jni/Graphics.cpp @@ -9,6 +9,7 @@ #include "GraphicsJNI.h" #include "SkCanvas.h" +#include "SkFontMetrics.h" #include "SkMath.h" #include "SkRegion.h" #include <cutils/ashmem.h> @@ -228,6 +229,20 @@ static jfieldID gColorSpace_Named_LinearExtendedSRGBFieldID; static jclass gTransferParameters_class; static jmethodID gTransferParameters_constructorMethodID; +static jclass gFontMetrics_class; +static jfieldID gFontMetrics_top; +static jfieldID gFontMetrics_ascent; +static jfieldID gFontMetrics_descent; +static jfieldID gFontMetrics_bottom; +static jfieldID gFontMetrics_leading; + +static jclass gFontMetricsInt_class; +static jfieldID gFontMetricsInt_top; +static jfieldID gFontMetricsInt_ascent; +static jfieldID gFontMetricsInt_descent; +static jfieldID gFontMetricsInt_bottom; +static jfieldID gFontMetricsInt_leading; + /////////////////////////////////////////////////////////////////////////////// void GraphicsJNI::get_jrect(JNIEnv* env, jobject obj, int* L, int* T, int* R, int* B) @@ -468,9 +483,35 @@ SkRegion* GraphicsJNI::getNativeRegion(JNIEnv* env, jobject region) return r; } +void GraphicsJNI::set_metrics(JNIEnv* env, jobject metrics, const SkFontMetrics& skmetrics) { + if (metrics == nullptr) return; + SkASSERT(env->IsInstanceOf(metrics, gFontMetrics_class)); + env->SetFloatField(metrics, gFontMetrics_top, SkScalarToFloat(skmetrics.fTop)); + env->SetFloatField(metrics, gFontMetrics_ascent, SkScalarToFloat(skmetrics.fAscent)); + env->SetFloatField(metrics, gFontMetrics_descent, SkScalarToFloat(skmetrics.fDescent)); + env->SetFloatField(metrics, gFontMetrics_bottom, SkScalarToFloat(skmetrics.fBottom)); + env->SetFloatField(metrics, gFontMetrics_leading, SkScalarToFloat(skmetrics.fLeading)); +} + +int GraphicsJNI::set_metrics_int(JNIEnv* env, jobject metrics, const SkFontMetrics& skmetrics) { + int ascent = SkScalarRoundToInt(skmetrics.fAscent); + int descent = SkScalarRoundToInt(skmetrics.fDescent); + int leading = SkScalarRoundToInt(skmetrics.fLeading); + + if (metrics) { + SkASSERT(env->IsInstanceOf(metrics, gFontMetricsInt_class)); + env->SetIntField(metrics, gFontMetricsInt_top, SkScalarFloorToInt(skmetrics.fTop)); + env->SetIntField(metrics, gFontMetricsInt_ascent, ascent); + env->SetIntField(metrics, gFontMetricsInt_descent, descent); + env->SetIntField(metrics, gFontMetricsInt_bottom, SkScalarCeilToInt(skmetrics.fBottom)); + env->SetIntField(metrics, gFontMetricsInt_leading, leading); + } + return descent - ascent + leading; +} + /////////////////////////////////////////////////////////////////////////////////////////// -jobject GraphicsJNI::createBitmapRegionDecoder(JNIEnv* env, SkBitmapRegionDecoder* bitmap) +jobject GraphicsJNI::createBitmapRegionDecoder(JNIEnv* env, skia::BitmapRegionDecoder* bitmap) { ALOG_ASSERT(bitmap != NULL); @@ -764,5 +805,23 @@ int register_android_graphics_Graphics(JNIEnv* env) gTransferParameters_constructorMethodID = GetMethodIDOrDie(env, gTransferParameters_class, "<init>", "(DDDDDDD)V"); + gFontMetrics_class = FindClassOrDie(env, "android/graphics/Paint$FontMetrics"); + gFontMetrics_class = MakeGlobalRefOrDie(env, gFontMetrics_class); + + gFontMetrics_top = GetFieldIDOrDie(env, gFontMetrics_class, "top", "F"); + gFontMetrics_ascent = GetFieldIDOrDie(env, gFontMetrics_class, "ascent", "F"); + gFontMetrics_descent = GetFieldIDOrDie(env, gFontMetrics_class, "descent", "F"); + gFontMetrics_bottom = GetFieldIDOrDie(env, gFontMetrics_class, "bottom", "F"); + gFontMetrics_leading = GetFieldIDOrDie(env, gFontMetrics_class, "leading", "F"); + + gFontMetricsInt_class = FindClassOrDie(env, "android/graphics/Paint$FontMetricsInt"); + gFontMetricsInt_class = MakeGlobalRefOrDie(env, gFontMetricsInt_class); + + gFontMetricsInt_top = GetFieldIDOrDie(env, gFontMetricsInt_class, "top", "I"); + gFontMetricsInt_ascent = GetFieldIDOrDie(env, gFontMetricsInt_class, "ascent", "I"); + gFontMetricsInt_descent = GetFieldIDOrDie(env, gFontMetricsInt_class, "descent", "I"); + gFontMetricsInt_bottom = GetFieldIDOrDie(env, gFontMetricsInt_class, "bottom", "I"); + gFontMetricsInt_leading = GetFieldIDOrDie(env, gFontMetricsInt_class, "leading", "I"); + return 0; } diff --git a/libs/hwui/jni/GraphicsJNI.h b/libs/hwui/jni/GraphicsJNI.h index b58a740a4c27..541d5a53de07 100644 --- a/libs/hwui/jni/GraphicsJNI.h +++ b/libs/hwui/jni/GraphicsJNI.h @@ -4,8 +4,8 @@ #include <cutils/compiler.h> #include "Bitmap.h" +#include "BRDAllocator.h" #include "SkBitmap.h" -#include "SkBRDAllocator.h" #include "SkCodec.h" #include "SkPixelRef.h" #include "SkMallocPixelRef.h" @@ -17,10 +17,13 @@ #include "graphics_jni_helpers.h" -class SkBitmapRegionDecoder; class SkCanvas; +struct SkFontMetrics; namespace android { +namespace skia { + class BitmapRegionDecoder; +} class Paint; struct Typeface; } @@ -83,6 +86,17 @@ public: bool* isHardware); static SkRegion* getNativeRegion(JNIEnv*, jobject region); + /** + * Set SkFontMetrics to Java Paint.FontMetrics. + * Do nothing if metrics is nullptr. + */ + static void set_metrics(JNIEnv*, jobject metrics, const SkFontMetrics& skmetrics); + /** + * Set SkFontMetrics to Java Paint.FontMetricsInt and return recommended interline space. + * Do nothing if metrics is nullptr. + */ + static int set_metrics_int(JNIEnv*, jobject metrics, const SkFontMetrics& skmetrics); + /* * LegacyBitmapConfig is the old enum in Skia that matched the enum int values * in Bitmap.Config. Skia no longer supports this config, but has replaced it @@ -103,7 +117,8 @@ public: static jobject createRegion(JNIEnv* env, SkRegion* region); - static jobject createBitmapRegionDecoder(JNIEnv* env, SkBitmapRegionDecoder* bitmap); + static jobject createBitmapRegionDecoder(JNIEnv* env, + android::skia::BitmapRegionDecoder* bitmap); /** * Given a bitmap we natively allocate a memory block to store the contents @@ -154,7 +169,7 @@ private: static JavaVM* mJavaVM; }; -class HeapAllocator : public SkBRDAllocator { +class HeapAllocator : public android::skia::BRDAllocator { public: HeapAllocator() { }; ~HeapAllocator() { }; @@ -181,7 +196,7 @@ private: * the decoded output to fit in the recycled bitmap if necessary. * This allocator implements that behavior. * - * Skia's SkBitmapRegionDecoder expects the memory that + * Skia's BitmapRegionDecoder expects the memory that * is allocated to be large enough to decode the entire region * that is requested. It will decode directly into the memory * that is provided. @@ -200,7 +215,7 @@ private: * reuse it again, given that it still may be in use from our * first allocation. */ -class RecyclingClippingPixelAllocator : public SkBRDAllocator { +class RecyclingClippingPixelAllocator : public android::skia::BRDAllocator { public: RecyclingClippingPixelAllocator(android::Bitmap* recycledBitmap, diff --git a/libs/hwui/jni/ImageDecoder.cpp b/libs/hwui/jni/ImageDecoder.cpp index c8c3d3d5b078..da91d46b0738 100644 --- a/libs/hwui/jni/ImageDecoder.cpp +++ b/libs/hwui/jni/ImageDecoder.cpp @@ -27,9 +27,9 @@ #include <hwui/ImageDecoder.h> #include <HardwareBitmapUploader.h> +#include <FrontBufferedStream.h> #include <SkAndroidCodec.h> #include <SkEncodedImageFormat.h> -#include <SkFrontBufferedStream.h> #include <SkStream.h> #include <androidfw/Asset.h> @@ -194,8 +194,7 @@ static jobject ImageDecoder_nCreateInputStream(JNIEnv* env, jobject /*clazz*/, } std::unique_ptr<SkStream> bufferedStream( - SkFrontBufferedStream::Make(std::move(stream), - SkCodec::MinBufferedBytesNeeded())); + skia::FrontBufferedStream::Make(std::move(stream), SkCodec::MinBufferedBytesNeeded())); return native_create(env, std::move(bufferedStream), source, preferAnimation); } diff --git a/libs/hwui/jni/Movie.cpp b/libs/hwui/jni/Movie.cpp index ede0ca8cda5b..bb8c99a73edf 100644 --- a/libs/hwui/jni/Movie.cpp +++ b/libs/hwui/jni/Movie.cpp @@ -1,7 +1,7 @@ #include "CreateJavaOutputStreamAdaptor.h" +#include "FrontBufferedStream.h" #include "GraphicsJNI.h" #include <nativehelper/ScopedLocalRef.h> -#include "SkFrontBufferedStream.h" #include "Movie.h" #include "SkStream.h" #include "SkUtils.h" @@ -100,10 +100,8 @@ static jobject movie_decodeStream(JNIEnv* env, jobject clazz, jobject istream) { // Need to buffer enough input to be able to rewind as much as might be read by a decoder // trying to determine the stream's format. The only decoder for movies is GIF, which // will only read 6. - // FIXME: Get this number from SkImageDecoder - // bufferedStream takes ownership of strm - std::unique_ptr<SkStreamRewindable> bufferedStream(SkFrontBufferedStream::Make( - std::unique_ptr<SkStream>(strm), 6)); + std::unique_ptr<SkStreamRewindable> bufferedStream( + android::skia::FrontBufferedStream::Make(std::unique_ptr<SkStream>(strm), 6)); SkASSERT(bufferedStream.get() != NULL); Movie* moov = Movie::DecodeStream(bufferedStream.get()); diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp index df8635a8fe5a..3c86b28262b0 100644 --- a/libs/hwui/jni/Paint.cpp +++ b/libs/hwui/jni/Paint.cpp @@ -56,20 +56,6 @@ namespace android { -struct JMetricsID { - jfieldID top; - jfieldID ascent; - jfieldID descent; - jfieldID bottom; - jfieldID leading; -}; - -static jclass gFontMetrics_class; -static JMetricsID gFontMetrics_fieldID; - -static jclass gFontMetricsInt_class; -static JMetricsID gFontMetricsInt_fieldID; - static void getPosTextPath(const SkFont& font, const uint16_t glyphs[], int count, const SkPoint pos[], SkPath* dst) { dst->reset(); @@ -353,18 +339,13 @@ namespace PaintGlue { } static void doTextBounds(JNIEnv* env, const jchar* text, int count, jobject bounds, - const Paint& paint, const Typeface* typeface, jint bidiFlags) { + const Paint& paint, const Typeface* typeface, jint bidiFlagsInt) { SkRect r; SkIRect ir; - minikin::Layout layout = MinikinUtils::doLayout(&paint, - static_cast<minikin::Bidi>(bidiFlags), typeface, - text, count, // text buffer - 0, count, // draw range - 0, count, // context range - nullptr); minikin::MinikinRect rect; - layout.getBounds(&rect); + minikin::Bidi bidiFlags = static_cast<minikin::Bidi>(bidiFlagsInt); + MinikinUtils::getBounds(&paint, bidiFlags, typeface, text, count, &rect); r.fLeft = rect.mLeft; r.fTop = rect.mTop; r.fRight = rect.mRight; @@ -615,35 +596,14 @@ namespace PaintGlue { static jfloat getFontMetrics(JNIEnv* env, jobject, jlong paintHandle, jobject metricsObj) { SkFontMetrics metrics; SkScalar spacing = getMetricsInternal(paintHandle, &metrics); - - if (metricsObj) { - SkASSERT(env->IsInstanceOf(metricsObj, gFontMetrics_class)); - env->SetFloatField(metricsObj, gFontMetrics_fieldID.top, SkScalarToFloat(metrics.fTop)); - env->SetFloatField(metricsObj, gFontMetrics_fieldID.ascent, SkScalarToFloat(metrics.fAscent)); - env->SetFloatField(metricsObj, gFontMetrics_fieldID.descent, SkScalarToFloat(metrics.fDescent)); - env->SetFloatField(metricsObj, gFontMetrics_fieldID.bottom, SkScalarToFloat(metrics.fBottom)); - env->SetFloatField(metricsObj, gFontMetrics_fieldID.leading, SkScalarToFloat(metrics.fLeading)); - } + GraphicsJNI::set_metrics(env, metricsObj, metrics); return SkScalarToFloat(spacing); } static jint getFontMetricsInt(JNIEnv* env, jobject, jlong paintHandle, jobject metricsObj) { SkFontMetrics metrics; - getMetricsInternal(paintHandle, &metrics); - int ascent = SkScalarRoundToInt(metrics.fAscent); - int descent = SkScalarRoundToInt(metrics.fDescent); - int leading = SkScalarRoundToInt(metrics.fLeading); - - if (metricsObj) { - SkASSERT(env->IsInstanceOf(metricsObj, gFontMetricsInt_class)); - env->SetIntField(metricsObj, gFontMetricsInt_fieldID.top, SkScalarFloorToInt(metrics.fTop)); - env->SetIntField(metricsObj, gFontMetricsInt_fieldID.ascent, ascent); - env->SetIntField(metricsObj, gFontMetricsInt_fieldID.descent, descent); - env->SetIntField(metricsObj, gFontMetricsInt_fieldID.bottom, SkScalarCeilToInt(metrics.fBottom)); - env->SetIntField(metricsObj, gFontMetricsInt_fieldID.leading, leading); - } - return descent - ascent + leading; + return GraphicsJNI::set_metrics_int(env, metricsObj, metrics); } @@ -1135,24 +1095,6 @@ static const JNINativeMethod methods[] = { }; int register_android_graphics_Paint(JNIEnv* env) { - gFontMetrics_class = FindClassOrDie(env, "android/graphics/Paint$FontMetrics"); - gFontMetrics_class = MakeGlobalRefOrDie(env, gFontMetrics_class); - - gFontMetrics_fieldID.top = GetFieldIDOrDie(env, gFontMetrics_class, "top", "F"); - gFontMetrics_fieldID.ascent = GetFieldIDOrDie(env, gFontMetrics_class, "ascent", "F"); - gFontMetrics_fieldID.descent = GetFieldIDOrDie(env, gFontMetrics_class, "descent", "F"); - gFontMetrics_fieldID.bottom = GetFieldIDOrDie(env, gFontMetrics_class, "bottom", "F"); - gFontMetrics_fieldID.leading = GetFieldIDOrDie(env, gFontMetrics_class, "leading", "F"); - - gFontMetricsInt_class = FindClassOrDie(env, "android/graphics/Paint$FontMetricsInt"); - gFontMetricsInt_class = MakeGlobalRefOrDie(env, gFontMetricsInt_class); - - gFontMetricsInt_fieldID.top = GetFieldIDOrDie(env, gFontMetricsInt_class, "top", "I"); - gFontMetricsInt_fieldID.ascent = GetFieldIDOrDie(env, gFontMetricsInt_class, "ascent", "I"); - gFontMetricsInt_fieldID.descent = GetFieldIDOrDie(env, gFontMetricsInt_class, "descent", "I"); - gFontMetricsInt_fieldID.bottom = GetFieldIDOrDie(env, gFontMetricsInt_class, "bottom", "I"); - gFontMetricsInt_fieldID.leading = GetFieldIDOrDie(env, gFontMetricsInt_class, "leading", "I"); - return RegisterMethodsOrDie(env, "android/graphics/Paint", methods, NELEM(methods)); } diff --git a/libs/hwui/jni/Picture.cpp b/libs/hwui/jni/Picture.cpp index d1b952130e88..8e4203c0b115 100644 --- a/libs/hwui/jni/Picture.cpp +++ b/libs/hwui/jni/Picture.cpp @@ -111,7 +111,7 @@ sk_sp<SkPicture> Picture::makePartialCopy() const { SkPictureRecorder reRecorder; - SkCanvas* canvas = reRecorder.beginRecording(mWidth, mHeight, NULL, 0); + SkCanvas* canvas = reRecorder.beginRecording(mWidth, mHeight); mRecorder->partialReplay(canvas); return reRecorder.finishRecordingAsPicture(); } diff --git a/libs/hwui/jni/RenderEffect.cpp b/libs/hwui/jni/RenderEffect.cpp new file mode 100644 index 000000000000..0ebd0ca720d8 --- /dev/null +++ b/libs/hwui/jni/RenderEffect.cpp @@ -0,0 +1,69 @@ +/* + * 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. + */ +#include "Bitmap.h" +#include "GraphicsJNI.h" +#include "SkImageFilter.h" +#include "SkImageFilters.h" +#include "graphics_jni_helpers.h" +#include "utils/Blur.h" +#include <utils/Log.h> + +using namespace android::uirenderer; + +static jlong createOffsetEffect( + JNIEnv* env, + jobject, + jfloat offsetX, + jfloat offsetY, + jlong inputFilterHandle +) { + auto* inputFilter = reinterpret_cast<const SkImageFilter*>(inputFilterHandle); + sk_sp<SkImageFilter> offset = SkImageFilters::Offset(offsetX, offsetY, sk_ref_sp(inputFilter)); + return reinterpret_cast<jlong>(offset.release()); +} + +static jlong createBlurEffect(JNIEnv* env , jobject, jfloat radiusX, + jfloat radiusY, jlong inputFilterHandle, jint edgeTreatment) { + auto* inputImageFilter = reinterpret_cast<SkImageFilter*>(inputFilterHandle); + sk_sp<SkImageFilter> blurFilter = + SkImageFilters::Blur( + Blur::convertRadiusToSigma(radiusX), + Blur::convertRadiusToSigma(radiusY), + static_cast<SkTileMode>(edgeTreatment), + sk_ref_sp(inputImageFilter), + nullptr); + return reinterpret_cast<jlong>(blurFilter.release()); +} + +static void RenderEffect_safeUnref(SkImageFilter* filter) { + SkSafeUnref(filter); +} + +static jlong getRenderEffectFinalizer(JNIEnv*, jobject) { + return static_cast<jlong>(reinterpret_cast<uintptr_t>(&RenderEffect_safeUnref)); +} + +static const JNINativeMethod gRenderEffectMethods[] = { + {"nativeGetFinalizer", "()J", (void*)getRenderEffectFinalizer}, + {"nativeCreateOffsetEffect", "(FFJ)J", (void*)createOffsetEffect}, + {"nativeCreateBlurEffect", "(FFJI)J", (void*)createBlurEffect} +}; + +int register_android_graphics_RenderEffect(JNIEnv* env) { + android::RegisterMethodsOrDie(env, "android/graphics/RenderEffect", + gRenderEffectMethods, NELEM(gRenderEffectMethods)); + return 0; +}
\ No newline at end of file diff --git a/libs/hwui/jni/Shader.cpp b/libs/hwui/jni/Shader.cpp index 0f6837640524..45795ff255aa 100644 --- a/libs/hwui/jni/Shader.cpp +++ b/libs/hwui/jni/Shader.cpp @@ -133,11 +133,25 @@ static jlong LinearGradient_create(JNIEnv* env, jobject, jlong matrixPtr, /////////////////////////////////////////////////////////////////////////////////////////////// -static jlong RadialGradient_create(JNIEnv* env, jobject, jlong matrixPtr, jfloat x, jfloat y, - jfloat radius, jlongArray colorArray, jfloatArray posArray, jint tileMode, +static jlong RadialGradient_create(JNIEnv* env, + jobject, + jlong matrixPtr, + jfloat startX, + jfloat startY, + jfloat startRadius, + jfloat endX, + jfloat endY, + jfloat endRadius, + jlongArray colorArray, + jfloatArray posArray, + jint tileMode, jlong colorSpaceHandle) { - SkPoint center; - center.set(x, y); + + SkPoint start; + start.set(startX, startY); + + SkPoint end; + end.set(endX, endY); std::vector<SkColor4f> colors = convertColorLongs(env, colorArray); @@ -148,11 +162,17 @@ static jlong RadialGradient_create(JNIEnv* env, jobject, jlong matrixPtr, jfloat #error Need to convert float array to SkScalar array before calling the following function. #endif - sk_sp<SkShader> shader = SkGradientShader::MakeRadial(center, radius, &colors[0], - GraphicsJNI::getNativeColorSpace(colorSpaceHandle), pos, colors.size(), - static_cast<SkTileMode>(tileMode), sGradientShaderFlags, nullptr); + auto colorSpace = GraphicsJNI::getNativeColorSpace(colorSpaceHandle); + auto skTileMode = static_cast<SkTileMode>(tileMode); + sk_sp<SkShader> shader = SkGradientShader::MakeTwoPointConical(start, startRadius, end, + endRadius, &colors[0], std::move(colorSpace), pos, colors.size(), skTileMode, + sGradientShaderFlags, nullptr); ThrowIAE_IfNull(env, shader); + // Explicitly create a new shader with the specified matrix to match existing behavior. + // Passing in the matrix in the instantiation above can throw exceptions for non-invertible + // matrices. However, makeWithLocalMatrix will still allow for the shader to be created + // and skia handles null-shaders internally (i.e. is ignored) const SkMatrix* matrix = reinterpret_cast<const SkMatrix*>(matrixPtr); if (matrix) { shader = shader->makeWithLocalMatrix(*matrix); @@ -211,14 +231,26 @@ static jlong ComposeShader_create(JNIEnv* env, jobject o, jlong matrixPtr, /////////////////////////////////////////////////////////////////////////////////////////////// static jlong RuntimeShader_create(JNIEnv* env, jobject, jlong shaderFactory, jlong matrixPtr, - jbyteArray inputs, jlong colorSpaceHandle, jboolean isOpaque) { + jbyteArray inputs, jlongArray inputShaders, jlong colorSpaceHandle, jboolean isOpaque) { SkRuntimeEffect* effect = reinterpret_cast<SkRuntimeEffect*>(shaderFactory); AutoJavaByteArray arInputs(env, inputs); + std::vector<sk_sp<SkShader>> shaderVector; + if (inputShaders) { + jsize shaderCount = env->GetArrayLength(inputShaders); + shaderVector.resize(shaderCount); + jlong* arrayPtr = env->GetLongArrayElements(inputShaders, NULL); + for (int i = 0; i < shaderCount; i++) { + shaderVector[i] = sk_ref_sp(reinterpret_cast<SkShader*>(arrayPtr[i])); + } + env->ReleaseLongArrayElements(inputShaders, arrayPtr, 0); + } + sk_sp<SkData> fData; fData = SkData::MakeWithCopy(arInputs.ptr(), arInputs.length()); const SkMatrix* matrix = reinterpret_cast<const SkMatrix*>(matrixPtr); - sk_sp<SkShader> shader = effect->makeShader(fData, nullptr, 0, matrix, isOpaque == JNI_TRUE); + sk_sp<SkShader> shader = effect->makeShader(fData, shaderVector.data(), shaderVector.size(), + matrix, isOpaque == JNI_TRUE); ThrowIAE_IfNull(env, shader); return reinterpret_cast<jlong>(shader.release()); @@ -228,9 +260,12 @@ static jlong RuntimeShader_create(JNIEnv* env, jobject, jlong shaderFactory, jlo static jlong RuntimeShader_createShaderFactory(JNIEnv* env, jobject, jstring sksl) { ScopedUtfChars strSksl(env, sksl); - sk_sp<SkRuntimeEffect> effect = std::get<0>(SkRuntimeEffect::Make(SkString(strSksl.c_str()))); - ThrowIAE_IfNull(env, effect); - + auto result = SkRuntimeEffect::Make(SkString(strSksl.c_str())); + sk_sp<SkRuntimeEffect> effect = std::get<0>(result); + if (!effect) { + const auto& err = std::get<1>(result); + doThrowIAE(env, err.c_str()); + } return reinterpret_cast<jlong>(effect.release()); } @@ -264,7 +299,7 @@ static const JNINativeMethod gLinearGradientMethods[] = { }; static const JNINativeMethod gRadialGradientMethods[] = { - { "nativeCreate", "(JFFF[J[FIJ)J", (void*)RadialGradient_create }, + { "nativeCreate", "(JFFFFFF[J[FIJ)J", (void*)RadialGradient_create }, }; static const JNINativeMethod gSweepGradientMethods[] = { @@ -277,7 +312,7 @@ static const JNINativeMethod gComposeShaderMethods[] = { static const JNINativeMethod gRuntimeShaderMethods[] = { { "nativeGetFinalizer", "()J", (void*)RuntimeShader_getNativeFinalizer }, - { "nativeCreate", "(JJ[BJZ)J", (void*)RuntimeShader_create }, + { "nativeCreate", "(JJ[B[JJZ)J", (void*)RuntimeShader_create }, { "nativeCreateShaderFactory", "(Ljava/lang/String;)J", (void*)RuntimeShader_createShaderFactory }, }; diff --git a/libs/hwui/jni/Utils.cpp b/libs/hwui/jni/Utils.cpp index 34fd6687d52c..ac2f5b77d23a 100644 --- a/libs/hwui/jni/Utils.cpp +++ b/libs/hwui/jni/Utils.cpp @@ -114,7 +114,7 @@ size_t AssetStreamAdaptor::read(void* buffer, size_t size) { return amount; } -SkMemoryStream* android::CopyAssetToStream(Asset* asset) { +sk_sp<SkData> android::CopyAssetToData(Asset* asset) { if (NULL == asset) { return NULL; } @@ -138,7 +138,7 @@ SkMemoryStream* android::CopyAssetToStream(Asset* asset) { return NULL; } - return new SkMemoryStream(std::move(data)); + return data; } jobject android::nullObjectReturn(const char msg[]) { diff --git a/libs/hwui/jni/Utils.h b/libs/hwui/jni/Utils.h index f628cc3c85ed..6cdf44d85a5a 100644 --- a/libs/hwui/jni/Utils.h +++ b/libs/hwui/jni/Utils.h @@ -46,12 +46,11 @@ private: }; /** - * Make a deep copy of the asset, and return it as a stream, or NULL if there + * Make a deep copy of the asset, and return it as an SkData, or NULL if there * was an error. - * FIXME: If we could "ref/reopen" the asset, we may not need to copy it here. */ -SkMemoryStream* CopyAssetToStream(Asset*); +sk_sp<SkData> CopyAssetToData(Asset*); /** Restore the file descriptor's offset in our destructor */ diff --git a/libs/hwui/jni/android_graphics_Canvas.cpp b/libs/hwui/jni/android_graphics_Canvas.cpp index b6c6cd0b5c1c..c04340c36511 100644 --- a/libs/hwui/jni/android_graphics_Canvas.cpp +++ b/libs/hwui/jni/android_graphics_Canvas.cpp @@ -30,6 +30,7 @@ #include <nativehelper/ScopedPrimitiveArray.h> #include <nativehelper/ScopedStringChars.h> +#include "FontUtils.h" #include "Bitmap.h" #include "SkGraphics.h" #include "SkRegion.h" @@ -540,6 +541,21 @@ static void drawBitmapMesh(JNIEnv* env, jobject, jlong canvasHandle, jlong bitma colorA.ptr() + colorIndex, paint); } +static void drawGlyphs(JNIEnv* env, jobject, jlong canvasHandle, jintArray glyphIds, + jfloatArray positions, jint glyphOffset, jint positionOffset, + jint glyphCount, jlong fontHandle, jlong paintHandle) { + Paint* paint = reinterpret_cast<Paint*>(paintHandle); + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontHandle); + AutoJavaIntArray glyphIdArray(env, glyphIds); + AutoJavaFloatArray positionArray(env, positions); + get_canvas(canvasHandle)->drawGlyphs( + *font->font.get(), + glyphIdArray.ptr() + glyphOffset, + positionArray.ptr() + positionOffset, + glyphCount, + *paint); +} + static void drawTextChars(JNIEnv* env, jobject, jlong canvasHandle, jcharArray charArray, jint index, jint count, jfloat x, jfloat y, jint bidiFlags, jlong paintHandle) { @@ -719,6 +735,7 @@ static const JNINativeMethod gDrawMethods[] = { {"nDrawBitmap","(JJFFJIII)V", (void*) CanvasJNI::drawBitmap}, {"nDrawBitmap","(JJFFFFFFFFJII)V", (void*) CanvasJNI::drawBitmapRect}, {"nDrawBitmap", "(J[IIIFFIIZJ)V", (void*)CanvasJNI::drawBitmapArray}, + {"nDrawGlyphs", "(J[I[FIIIJJ)V", (void*)CanvasJNI::drawGlyphs}, {"nDrawText","(J[CIIFFIJ)V", (void*) CanvasJNI::drawTextChars}, {"nDrawText","(JLjava/lang/String;IIFFIJ)V", (void*) CanvasJNI::drawTextString}, {"nDrawTextRun","(J[CIIIIFFZJJ)V", (void*) CanvasJNI::drawTextRunChars}, diff --git a/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp b/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp index 54822f1f07e2..7c1422de0984 100644 --- a/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp +++ b/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp @@ -67,39 +67,7 @@ private: JavaVM* mVm; jobject mRunnable; }; - -class GlFunctorReleasedCallbackBridge : public GlFunctorLifecycleListener { -public: - GlFunctorReleasedCallbackBridge(JNIEnv* env, jobject javaCallback) { - mLooper = Looper::getForThread(); - mMessage = new InvokeRunnableMessage(env, javaCallback); - } - - virtual void onGlFunctorReleased(Functor* functor) override { - mLooper->sendMessage(mMessage, 0); - } - -private: - sp<Looper> mLooper; - sp<InvokeRunnableMessage> mMessage; -}; -#endif - -// ---------------- @FastNative ----------------------------- - -static void android_view_DisplayListCanvas_callDrawGLFunction(JNIEnv* env, jobject clazz, - jlong canvasPtr, jlong functorPtr, jobject releasedCallback) { -#ifdef __ANDROID__ // Layoutlib does not support GL - Canvas* canvas = reinterpret_cast<Canvas*>(canvasPtr); - Functor* functor = reinterpret_cast<Functor*>(functorPtr); - sp<GlFunctorReleasedCallbackBridge> bridge; - if (releasedCallback) { - bridge = new GlFunctorReleasedCallbackBridge(env, releasedCallback); - } - canvas->callDrawGLFunction(functor, bridge.get()); #endif -} - // ---------------- @CriticalNative ------------------------- @@ -124,10 +92,10 @@ static jint android_view_DisplayListCanvas_getMaxTextureSize(CRITICAL_JNI_PARAMS #endif } -static void android_view_DisplayListCanvas_insertReorderBarrier(CRITICAL_JNI_PARAMS_COMMA jlong canvasPtr, +static void android_view_DisplayListCanvas_enableZ(CRITICAL_JNI_PARAMS_COMMA jlong canvasPtr, jboolean reorderEnable) { Canvas* canvas = reinterpret_cast<Canvas*>(canvasPtr); - canvas->insertReorderBarrier(reorderEnable); + canvas->enableZ(reorderEnable); } static jlong android_view_DisplayListCanvas_finishRecording(CRITICAL_JNI_PARAMS_COMMA jlong canvasPtr) { @@ -183,18 +151,12 @@ static void android_view_DisplayListCanvas_drawWebViewFunctor(CRITICAL_JNI_PARAM const char* const kClassPathName = "android/graphics/RecordingCanvas"; static JNINativeMethod gMethods[] = { - - // ------------ @FastNative ------------------ - - { "nCallDrawGLFunction", "(JJLjava/lang/Runnable;)V", - (void*) android_view_DisplayListCanvas_callDrawGLFunction }, - // ------------ @CriticalNative -------------- { "nCreateDisplayListCanvas", "(JII)J", (void*) android_view_DisplayListCanvas_createDisplayListCanvas }, { "nResetDisplayListCanvas", "(JJII)V", (void*) android_view_DisplayListCanvas_resetDisplayListCanvas }, { "nGetMaximumTextureWidth", "()I", (void*) android_view_DisplayListCanvas_getMaxTextureSize }, { "nGetMaximumTextureHeight", "()I", (void*) android_view_DisplayListCanvas_getMaxTextureSize }, - { "nInsertReorderBarrier", "(JZ)V", (void*) android_view_DisplayListCanvas_insertReorderBarrier }, + { "nEnableZ", "(JZ)V", (void*) android_view_DisplayListCanvas_enableZ }, { "nFinishRecording", "(J)J", (void*) android_view_DisplayListCanvas_finishRecording }, { "nDrawRenderNode", "(JJ)V", (void*) android_view_DisplayListCanvas_drawRenderNode }, { "nDrawTextureLayer", "(JJ)V", (void*) android_view_DisplayListCanvas_drawTextureLayer }, diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp index 9815e85db880..a146b64e29cc 100644 --- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp +++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp @@ -143,11 +143,10 @@ static jlong android_view_ThreadedRenderer_createRootRenderNode(JNIEnv* env, job } static jlong android_view_ThreadedRenderer_createProxy(JNIEnv* env, jobject clazz, - jboolean translucent, jboolean isWideGamut, jlong rootRenderNodePtr) { + jboolean translucent, jlong rootRenderNodePtr) { RootRenderNode* rootRenderNode = reinterpret_cast<RootRenderNode*>(rootRenderNodePtr); ContextFactoryImpl factory(rootRenderNode); RenderProxy* proxy = new RenderProxy(translucent, rootRenderNode, &factory); - proxy->setWideGamut(isWideGamut); return (jlong) proxy; } @@ -185,7 +184,9 @@ static void android_view_ThreadedRenderer_setSurface(JNIEnv* env, jobject clazz, proxy->setSwapBehavior(SwapBehavior::kSwap_discardBuffer); } proxy->setSurface(window, enableTimeout); - ANativeWindow_release(window); + if (window) { + ANativeWindow_release(window); + } } static jboolean android_view_ThreadedRenderer_pause(JNIEnv* env, jobject clazz, @@ -218,10 +219,15 @@ static void android_view_ThreadedRenderer_setOpaque(JNIEnv* env, jobject clazz, proxy->setOpaque(opaque); } -static void android_view_ThreadedRenderer_setWideGamut(JNIEnv* env, jobject clazz, - jlong proxyPtr, jboolean wideGamut) { +static void android_view_ThreadedRenderer_setColorMode(JNIEnv* env, jobject clazz, + jlong proxyPtr, jint colorMode) { RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr); - proxy->setWideGamut(wideGamut); + proxy->setColorMode(static_cast<ColorMode>(colorMode)); +} + +static void android_view_ThreadedRenderer_setSdrWhitePoint(JNIEnv* env, jobject clazz, + jlong proxyPtr, jfloat sdrWhitePoint) { + Properties::defaultSdrWhitePoint = sdrWhitePoint; } static int android_view_ThreadedRenderer_syncAndDrawFrame(JNIEnv* env, jobject clazz, @@ -256,12 +262,6 @@ static void android_view_ThreadedRenderer_registerVectorDrawableAnimator(JNIEnv* rootRenderNode->addVectorDrawableAnimator(animator); } -static void android_view_ThreadedRenderer_invokeFunctor(JNIEnv* env, jobject clazz, - jlong functorPtr, jboolean waitForCompletion) { - Functor* functor = reinterpret_cast<Functor*>(functorPtr); - RenderProxy::invokeFunctor(functor, waitForCompletion); -} - static jlong android_view_ThreadedRenderer_createTextureLayer(JNIEnv* env, jobject clazz, jlong proxyPtr) { RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr); @@ -514,7 +514,8 @@ static jobject android_view_ThreadedRenderer_createHardwareBitmapFromRenderNode( proxy.setLightGeometry((Vector3){0, 0, 0}, 0); nsecs_t vsync = systemTime(SYSTEM_TIME_MONOTONIC); UiFrameInfoBuilder(proxy.frameInfo()) - .setVsync(vsync, vsync) + .setVsync(vsync, vsync, UiFrameInfoBuilder::INVALID_VSYNC_ID, + std::numeric_limits<int64_t>::max()) .addFlag(FrameInfoFlags::SurfaceCanvas); proxy.syncAndDrawFrame(); } @@ -593,6 +594,28 @@ static void android_view_ThreadedRenderer_preload(JNIEnv*, jclass) { RenderProxy::preload(); } +// Plumbs the display density down to DeviceInfo. +static void android_view_ThreadedRenderer_setDisplayDensityDpi(JNIEnv*, jclass, jint densityDpi) { + // Convert from dpi to density-independent pixels. + const float density = densityDpi / 160.0; + DeviceInfo::setDensity(density); +} + +static void android_view_ThreadedRenderer_initDisplayInfo(JNIEnv*, jclass, jint physicalWidth, + jint physicalHeight, jfloat refreshRate, + jfloat maxRefreshRate, + jint wideColorDataspace, + jlong appVsyncOffsetNanos, + jlong presentationDeadlineNanos) { + DeviceInfo::setWidth(physicalWidth); + DeviceInfo::setHeight(physicalHeight); + DeviceInfo::setRefreshRate(refreshRate); + DeviceInfo::setMaxRefreshRate(maxRefreshRate); + DeviceInfo::setWideColorDataspace(static_cast<ADataSpace>(wideColorDataspace)); + DeviceInfo::setAppVsyncOffsetNanos(appVsyncOffsetNanos); + DeviceInfo::setPresentationDeadlineNanos(presentationDeadlineNanos); +} + // ---------------------------------------------------------------------------- // HardwareRendererObserver // ---------------------------------------------------------------------------- @@ -637,67 +660,83 @@ static void android_view_ThreadedRenderer_setupShadersDiskCache(JNIEnv* env, job const char* const kClassPathName = "android/graphics/HardwareRenderer"; static const JNINativeMethod gMethods[] = { - { "nRotateProcessStatsBuffer", "()V", (void*) android_view_ThreadedRenderer_rotateProcessStatsBuffer }, - { "nSetProcessStatsBuffer", "(I)V", (void*) android_view_ThreadedRenderer_setProcessStatsBuffer }, - { "nGetRenderThreadTid", "(J)I", (void*) android_view_ThreadedRenderer_getRenderThreadTid }, - { "nCreateRootRenderNode", "()J", (void*) android_view_ThreadedRenderer_createRootRenderNode }, - { "nCreateProxy", "(ZZJ)J", (void*) android_view_ThreadedRenderer_createProxy }, - { "nDeleteProxy", "(J)V", (void*) android_view_ThreadedRenderer_deleteProxy }, - { "nLoadSystemProperties", "(J)Z", (void*) android_view_ThreadedRenderer_loadSystemProperties }, - { "nSetName", "(JLjava/lang/String;)V", (void*) android_view_ThreadedRenderer_setName }, - { "nSetSurface", "(JLandroid/view/Surface;Z)V", (void*) android_view_ThreadedRenderer_setSurface }, - { "nPause", "(J)Z", (void*) android_view_ThreadedRenderer_pause }, - { "nSetStopped", "(JZ)V", (void*) android_view_ThreadedRenderer_setStopped }, - { "nSetLightAlpha", "(JFF)V", (void*) android_view_ThreadedRenderer_setLightAlpha }, - { "nSetLightGeometry", "(JFFFF)V", (void*) android_view_ThreadedRenderer_setLightGeometry }, - { "nSetOpaque", "(JZ)V", (void*) android_view_ThreadedRenderer_setOpaque }, - { "nSetWideGamut", "(JZ)V", (void*) android_view_ThreadedRenderer_setWideGamut }, - { "nSyncAndDrawFrame", "(J[JI)I", (void*) android_view_ThreadedRenderer_syncAndDrawFrame }, - { "nDestroy", "(JJ)V", (void*) android_view_ThreadedRenderer_destroy }, - { "nRegisterAnimatingRenderNode", "(JJ)V", (void*) android_view_ThreadedRenderer_registerAnimatingRenderNode }, - { "nRegisterVectorDrawableAnimator", "(JJ)V", (void*) android_view_ThreadedRenderer_registerVectorDrawableAnimator }, - { "nInvokeFunctor", "(JZ)V", (void*) android_view_ThreadedRenderer_invokeFunctor }, - { "nCreateTextureLayer", "(J)J", (void*) android_view_ThreadedRenderer_createTextureLayer }, - { "nBuildLayer", "(JJ)V", (void*) android_view_ThreadedRenderer_buildLayer }, - { "nCopyLayerInto", "(JJJ)Z", (void*) android_view_ThreadedRenderer_copyLayerInto }, - { "nPushLayerUpdate", "(JJ)V", (void*) android_view_ThreadedRenderer_pushLayerUpdate }, - { "nCancelLayerUpdate", "(JJ)V", (void*) android_view_ThreadedRenderer_cancelLayerUpdate }, - { "nDetachSurfaceTexture", "(JJ)V", (void*) android_view_ThreadedRenderer_detachSurfaceTexture }, - { "nDestroyHardwareResources", "(J)V", (void*) android_view_ThreadedRenderer_destroyHardwareResources }, - { "nTrimMemory", "(I)V", (void*) android_view_ThreadedRenderer_trimMemory }, - { "nOverrideProperty", "(Ljava/lang/String;Ljava/lang/String;)V", (void*) android_view_ThreadedRenderer_overrideProperty }, - { "nFence", "(J)V", (void*) android_view_ThreadedRenderer_fence }, - { "nStopDrawing", "(J)V", (void*) android_view_ThreadedRenderer_stopDrawing }, - { "nNotifyFramePending", "(J)V", (void*) android_view_ThreadedRenderer_notifyFramePending }, - { "nDumpProfileInfo", "(JLjava/io/FileDescriptor;I)V", (void*) android_view_ThreadedRenderer_dumpProfileInfo }, - { "setupShadersDiskCache", "(Ljava/lang/String;Ljava/lang/String;)V", - (void*) android_view_ThreadedRenderer_setupShadersDiskCache }, - { "nAddRenderNode", "(JJZ)V", (void*) android_view_ThreadedRenderer_addRenderNode}, - { "nRemoveRenderNode", "(JJ)V", (void*) android_view_ThreadedRenderer_removeRenderNode}, - { "nDrawRenderNode", "(JJ)V", (void*) android_view_ThreadedRendererd_drawRenderNode}, - { "nSetContentDrawBounds", "(JIIII)V", (void*)android_view_ThreadedRenderer_setContentDrawBounds}, - { "nSetPictureCaptureCallback", "(JLandroid/graphics/HardwareRenderer$PictureCapturedCallback;)V", - (void*) android_view_ThreadedRenderer_setPictureCapturedCallbackJNI }, - { "nSetFrameCallback", "(JLandroid/graphics/HardwareRenderer$FrameDrawingCallback;)V", - (void*)android_view_ThreadedRenderer_setFrameCallback}, - { "nSetFrameCompleteCallback", "(JLandroid/graphics/HardwareRenderer$FrameCompleteCallback;)V", - (void*)android_view_ThreadedRenderer_setFrameCompleteCallback }, - { "nAddObserver", "(JJ)V", (void*)android_view_ThreadedRenderer_addObserver }, - { "nRemoveObserver", "(JJ)V", (void*)android_view_ThreadedRenderer_removeObserver }, - { "nCopySurfaceInto", "(Landroid/view/Surface;IIIIJ)I", - (void*)android_view_ThreadedRenderer_copySurfaceInto }, - { "nCreateHardwareBitmap", "(JII)Landroid/graphics/Bitmap;", - (void*)android_view_ThreadedRenderer_createHardwareBitmapFromRenderNode }, - { "disableVsync", "()V", (void*)android_view_ThreadedRenderer_disableVsync }, - { "nSetHighContrastText", "(Z)V", (void*)android_view_ThreadedRenderer_setHighContrastText }, - { "nHackySetRTAnimationsEnabled", "(Z)V", - (void*)android_view_ThreadedRenderer_hackySetRTAnimationsEnabled }, - { "nSetDebuggingEnabled", "(Z)V", (void*)android_view_ThreadedRenderer_setDebuggingEnabled }, - { "nSetIsolatedProcess", "(Z)V", (void*)android_view_ThreadedRenderer_setIsolatedProcess }, - { "nSetContextPriority", "(I)V", (void*)android_view_ThreadedRenderer_setContextPriority }, - { "nAllocateBuffers", "(J)V", (void*)android_view_ThreadedRenderer_allocateBuffers }, - { "nSetForceDark", "(JZ)V", (void*)android_view_ThreadedRenderer_setForceDark }, - { "preload", "()V", (void*)android_view_ThreadedRenderer_preload }, + {"nRotateProcessStatsBuffer", "()V", + (void*)android_view_ThreadedRenderer_rotateProcessStatsBuffer}, + {"nSetProcessStatsBuffer", "(I)V", + (void*)android_view_ThreadedRenderer_setProcessStatsBuffer}, + {"nGetRenderThreadTid", "(J)I", (void*)android_view_ThreadedRenderer_getRenderThreadTid}, + {"nCreateRootRenderNode", "()J", (void*)android_view_ThreadedRenderer_createRootRenderNode}, + {"nCreateProxy", "(ZJ)J", (void*)android_view_ThreadedRenderer_createProxy}, + {"nDeleteProxy", "(J)V", (void*)android_view_ThreadedRenderer_deleteProxy}, + {"nLoadSystemProperties", "(J)Z", + (void*)android_view_ThreadedRenderer_loadSystemProperties}, + {"nSetName", "(JLjava/lang/String;)V", (void*)android_view_ThreadedRenderer_setName}, + {"nSetSurface", "(JLandroid/view/Surface;Z)V", + (void*)android_view_ThreadedRenderer_setSurface}, + {"nPause", "(J)Z", (void*)android_view_ThreadedRenderer_pause}, + {"nSetStopped", "(JZ)V", (void*)android_view_ThreadedRenderer_setStopped}, + {"nSetLightAlpha", "(JFF)V", (void*)android_view_ThreadedRenderer_setLightAlpha}, + {"nSetLightGeometry", "(JFFFF)V", (void*)android_view_ThreadedRenderer_setLightGeometry}, + {"nSetOpaque", "(JZ)V", (void*)android_view_ThreadedRenderer_setOpaque}, + {"nSetColorMode", "(JI)V", (void*)android_view_ThreadedRenderer_setColorMode}, + {"nSetSdrWhitePoint", "(JF)V", (void*)android_view_ThreadedRenderer_setSdrWhitePoint}, + {"nSyncAndDrawFrame", "(J[JI)I", (void*)android_view_ThreadedRenderer_syncAndDrawFrame}, + {"nDestroy", "(JJ)V", (void*)android_view_ThreadedRenderer_destroy}, + {"nRegisterAnimatingRenderNode", "(JJ)V", + (void*)android_view_ThreadedRenderer_registerAnimatingRenderNode}, + {"nRegisterVectorDrawableAnimator", "(JJ)V", + (void*)android_view_ThreadedRenderer_registerVectorDrawableAnimator}, + {"nCreateTextureLayer", "(J)J", (void*)android_view_ThreadedRenderer_createTextureLayer}, + {"nBuildLayer", "(JJ)V", (void*)android_view_ThreadedRenderer_buildLayer}, + {"nCopyLayerInto", "(JJJ)Z", (void*)android_view_ThreadedRenderer_copyLayerInto}, + {"nPushLayerUpdate", "(JJ)V", (void*)android_view_ThreadedRenderer_pushLayerUpdate}, + {"nCancelLayerUpdate", "(JJ)V", (void*)android_view_ThreadedRenderer_cancelLayerUpdate}, + {"nDetachSurfaceTexture", "(JJ)V", + (void*)android_view_ThreadedRenderer_detachSurfaceTexture}, + {"nDestroyHardwareResources", "(J)V", + (void*)android_view_ThreadedRenderer_destroyHardwareResources}, + {"nTrimMemory", "(I)V", (void*)android_view_ThreadedRenderer_trimMemory}, + {"nOverrideProperty", "(Ljava/lang/String;Ljava/lang/String;)V", + (void*)android_view_ThreadedRenderer_overrideProperty}, + {"nFence", "(J)V", (void*)android_view_ThreadedRenderer_fence}, + {"nStopDrawing", "(J)V", (void*)android_view_ThreadedRenderer_stopDrawing}, + {"nNotifyFramePending", "(J)V", (void*)android_view_ThreadedRenderer_notifyFramePending}, + {"nDumpProfileInfo", "(JLjava/io/FileDescriptor;I)V", + (void*)android_view_ThreadedRenderer_dumpProfileInfo}, + {"setupShadersDiskCache", "(Ljava/lang/String;Ljava/lang/String;)V", + (void*)android_view_ThreadedRenderer_setupShadersDiskCache}, + {"nAddRenderNode", "(JJZ)V", (void*)android_view_ThreadedRenderer_addRenderNode}, + {"nRemoveRenderNode", "(JJ)V", (void*)android_view_ThreadedRenderer_removeRenderNode}, + {"nDrawRenderNode", "(JJ)V", (void*)android_view_ThreadedRendererd_drawRenderNode}, + {"nSetContentDrawBounds", "(JIIII)V", + (void*)android_view_ThreadedRenderer_setContentDrawBounds}, + {"nSetPictureCaptureCallback", + "(JLandroid/graphics/HardwareRenderer$PictureCapturedCallback;)V", + (void*)android_view_ThreadedRenderer_setPictureCapturedCallbackJNI}, + {"nSetFrameCallback", "(JLandroid/graphics/HardwareRenderer$FrameDrawingCallback;)V", + (void*)android_view_ThreadedRenderer_setFrameCallback}, + {"nSetFrameCompleteCallback", + "(JLandroid/graphics/HardwareRenderer$FrameCompleteCallback;)V", + (void*)android_view_ThreadedRenderer_setFrameCompleteCallback}, + {"nAddObserver", "(JJ)V", (void*)android_view_ThreadedRenderer_addObserver}, + {"nRemoveObserver", "(JJ)V", (void*)android_view_ThreadedRenderer_removeObserver}, + {"nCopySurfaceInto", "(Landroid/view/Surface;IIIIJ)I", + (void*)android_view_ThreadedRenderer_copySurfaceInto}, + {"nCreateHardwareBitmap", "(JII)Landroid/graphics/Bitmap;", + (void*)android_view_ThreadedRenderer_createHardwareBitmapFromRenderNode}, + {"disableVsync", "()V", (void*)android_view_ThreadedRenderer_disableVsync}, + {"nSetHighContrastText", "(Z)V", (void*)android_view_ThreadedRenderer_setHighContrastText}, + {"nHackySetRTAnimationsEnabled", "(Z)V", + (void*)android_view_ThreadedRenderer_hackySetRTAnimationsEnabled}, + {"nSetDebuggingEnabled", "(Z)V", (void*)android_view_ThreadedRenderer_setDebuggingEnabled}, + {"nSetIsolatedProcess", "(Z)V", (void*)android_view_ThreadedRenderer_setIsolatedProcess}, + {"nSetContextPriority", "(I)V", (void*)android_view_ThreadedRenderer_setContextPriority}, + {"nAllocateBuffers", "(J)V", (void*)android_view_ThreadedRenderer_allocateBuffers}, + {"nSetForceDark", "(JZ)V", (void*)android_view_ThreadedRenderer_setForceDark}, + {"nSetDisplayDensityDpi", "(I)V", + (void*)android_view_ThreadedRenderer_setDisplayDensityDpi}, + {"nInitDisplayInfo", "(IIFFIJJ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo}, + {"preload", "()V", (void*)android_view_ThreadedRenderer_preload}, }; static JavaVM* mJvm = nullptr; diff --git a/libs/hwui/jni/android_graphics_RenderNode.cpp b/libs/hwui/jni/android_graphics_RenderNode.cpp index 85c802b40459..4b4aa92b97b7 100644 --- a/libs/hwui/jni/android_graphics_RenderNode.cpp +++ b/libs/hwui/jni/android_graphics_RenderNode.cpp @@ -215,6 +215,12 @@ static jboolean android_view_RenderNode_setAlpha(CRITICAL_JNI_PARAMS_COMMA jlong return SET_AND_DIRTY(setAlpha, alpha, RenderNode::ALPHA); } +static jboolean android_view_RenderNode_setRenderEffect(CRITICAL_JNI_PARAMS_COMMA jlong renderNodePtr, + jlong renderEffectPtr) { + SkImageFilter* imageFilter = reinterpret_cast<SkImageFilter*>(renderEffectPtr); + return SET_AND_DIRTY(mutateLayerProperties().setImageFilter, imageFilter, RenderNode::GENERIC); +} + static jboolean android_view_RenderNode_setHasOverlappingRendering(CRITICAL_JNI_PARAMS_COMMA jlong renderNodePtr, bool hasOverlappingRendering) { return SET_AND_DIRTY(setHasOverlappingRendering, hasOverlappingRendering, @@ -690,6 +696,7 @@ static const JNINativeMethod gMethods[] = { { "nSetRevealClip", "(JZFFF)Z", (void*) android_view_RenderNode_setRevealClip }, { "nSetAlpha", "(JF)Z", (void*) android_view_RenderNode_setAlpha }, + { "nSetRenderEffect", "(JJ)V", (void*) android_view_RenderNode_setRenderEffect }, { "nSetHasOverlappingRendering", "(JZ)Z", (void*) android_view_RenderNode_setHasOverlappingRendering }, { "nSetUsageHint", "(JI)V", (void*) android_view_RenderNode_setUsageHint }, diff --git a/libs/hwui/jni/android_graphics_TextureLayer.cpp b/libs/hwui/jni/android_graphics_TextureLayer.cpp index bd20269d3751..4dbb24ce4347 100644 --- a/libs/hwui/jni/android_graphics_TextureLayer.cpp +++ b/libs/hwui/jni/android_graphics_TextureLayer.cpp @@ -67,7 +67,7 @@ static void TextureLayer_updateSurfaceTexture(JNIEnv* env, jobject clazz, // JNI Glue // ---------------------------------------------------------------------------- -const char* const kClassPathName = "android/view/TextureLayer"; +const char* const kClassPathName = "android/graphics/TextureLayer"; static const JNINativeMethod gMethods[] = { { "nPrepare", "(JIIZ)Z", (void*) TextureLayer_prepare }, @@ -78,7 +78,7 @@ static const JNINativeMethod gMethods[] = { { "nUpdateSurfaceTexture", "(J)V", (void*) TextureLayer_updateSurfaceTexture }, }; -int register_android_view_TextureLayer(JNIEnv* env) { +int register_android_graphics_TextureLayer(JNIEnv* env) { return RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods)); } diff --git a/libs/hwui/jni/fonts/Font.cpp b/libs/hwui/jni/fonts/Font.cpp index 5714cd1d0390..4aee6b94a2be 100644 --- a/libs/hwui/jni/fonts/Font.cpp +++ b/libs/hwui/jni/fonts/Font.cpp @@ -18,6 +18,8 @@ #define LOG_TAG "Minikin" #include "SkData.h" +#include "SkFont.h" +#include "SkFontMetrics.h" #include "SkFontMgr.h" #include "SkRefCnt.h" #include "SkTypeface.h" @@ -27,6 +29,7 @@ #include "FontUtils.h" #include <hwui/MinikinSkia.h> +#include <hwui/Paint.h> #include <hwui/Typeface.h> #include <minikin/FontFamily.h> #include <ui/FatVector.h> @@ -93,19 +96,19 @@ static jlong Font_Builder_build(JNIEnv* env, jobject clazz, jlong builderPtr, jo sk_sp<SkData> data(SkData::MakeWithProc(fontPtr, fontSize, release_global_ref, reinterpret_cast<void*>(fontRef))); - FatVector<SkFontArguments::Axis, 2> skiaAxes; + FatVector<SkFontArguments::VariationPosition::Coordinate, 2> skVariation; for (const auto& axis : builder->axes) { - skiaAxes.emplace_back(SkFontArguments::Axis{axis.axisTag, axis.value}); + skVariation.push_back({axis.axisTag, axis.value}); } std::unique_ptr<SkStreamAsset> fontData(new SkMemoryStream(std::move(data))); - SkFontArguments params; - params.setCollectionIndex(ttcIndex); - params.setAxes(skiaAxes.data(), skiaAxes.size()); + SkFontArguments args; + args.setCollectionIndex(ttcIndex); + args.setVariationDesignPosition({skVariation.data(), static_cast<int>(skVariation.size())}); sk_sp<SkFontMgr> fm(SkFontMgr::RefDefault()); - sk_sp<SkTypeface> face(fm->makeFromStream(std::move(fontData), params)); + sk_sp<SkTypeface> face(fm->makeFromStream(std::move(fontData), args)); if (face == nullptr) { jniThrowException(env, "java/lang/IllegalArgumentException", "Failed to create internal object. maybe invalid font data."); @@ -115,11 +118,43 @@ static jlong Font_Builder_build(JNIEnv* env, jobject clazz, jlong builderPtr, jo std::make_shared<MinikinFontSkia>(std::move(face), fontPtr, fontSize, std::string_view(fontPath.c_str(), fontPath.size()), ttcIndex, builder->axes); - minikin::Font font = minikin::Font::Builder(minikinFont).setWeight(weight) + std::shared_ptr<minikin::Font> font = minikin::Font::Builder(minikinFont).setWeight(weight) .setSlant(static_cast<minikin::FontStyle::Slant>(italic)).build(); return reinterpret_cast<jlong>(new FontWrapper(std::move(font))); } +// Fast Native +static jlong Font_Builder_clone(JNIEnv* env, jobject clazz, jlong fontPtr, jlong builderPtr, + jint weight, jboolean italic, jint ttcIndex) { + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr); + MinikinFontSkia* minikinSkia = static_cast<MinikinFontSkia*>(font->font->typeface().get()); + std::unique_ptr<NativeFontBuilder> builder(toBuilder(builderPtr)); + + // Reconstruct SkTypeface with different arguments from existing SkTypeface. + FatVector<SkFontArguments::VariationPosition::Coordinate, 2> skVariation; + for (const auto& axis : builder->axes) { + skVariation.push_back({axis.axisTag, axis.value}); + } + SkFontArguments args; + args.setCollectionIndex(ttcIndex); + args.setVariationDesignPosition({skVariation.data(), static_cast<int>(skVariation.size())}); + + sk_sp<SkTypeface> newTypeface = minikinSkia->GetSkTypeface()->makeClone(args); + + std::shared_ptr<minikin::MinikinFont> newMinikinFont = std::make_shared<MinikinFontSkia>( + std::move(newTypeface), + minikinSkia->GetFontData(), + minikinSkia->GetFontSize(), + minikinSkia->getFilePath(), + minikinSkia->GetFontIndex(), + builder->axes); + std::shared_ptr<minikin::Font> newFont = minikin::Font::Builder(newMinikinFont) + .setWeight(weight) + .setSlant(static_cast<minikin::FontStyle::Slant>(italic)) + .build(); + return reinterpret_cast<jlong>(new FontWrapper(std::move(newFont))); +} + // Critical Native static jlong Font_Builder_getReleaseNativeFont(CRITICAL_JNI_PARAMS) { return reinterpret_cast<jlong>(releaseFont); @@ -127,16 +162,157 @@ static jlong Font_Builder_getReleaseNativeFont(CRITICAL_JNI_PARAMS) { /////////////////////////////////////////////////////////////////////////////// +// Fast Native +static jfloat Font_getGlyphBounds(JNIEnv* env, jobject, jlong fontHandle, jint glyphId, + jlong paintHandle, jobject rect) { + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontHandle); + MinikinFontSkia* minikinSkia = static_cast<MinikinFontSkia*>(font->font->typeface().get()); + Paint* paint = reinterpret_cast<Paint*>(paintHandle); + + SkFont* skFont = &paint->getSkFont(); + // We don't use populateSkFont since it is designed to be used for layout result with addressing + // auto fake-bolding. + skFont->setTypeface(minikinSkia->RefSkTypeface()); + + uint16_t glyph16 = glyphId; + SkRect skBounds; + SkScalar skWidth; + skFont->getWidthsBounds(&glyph16, 1, &skWidth, &skBounds, nullptr); + GraphicsJNI::rect_to_jrectf(skBounds, env, rect); + return SkScalarToFloat(skWidth); +} + +// Fast Native +static jfloat Font_getFontMetrics(JNIEnv* env, jobject, jlong fontHandle, jlong paintHandle, + jobject metricsObj) { + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontHandle); + MinikinFontSkia* minikinSkia = static_cast<MinikinFontSkia*>(font->font->typeface().get()); + Paint* paint = reinterpret_cast<Paint*>(paintHandle); + + SkFont* skFont = &paint->getSkFont(); + // We don't use populateSkFont since it is designed to be used for layout result with addressing + // auto fake-bolding. + skFont->setTypeface(minikinSkia->RefSkTypeface()); + + SkFontMetrics metrics; + SkScalar spacing = skFont->getMetrics(&metrics); + GraphicsJNI::set_metrics(env, metricsObj, metrics); + return spacing; +} + +// Critical Native +static jlong Font_getFontInfo(CRITICAL_JNI_PARAMS_COMMA jlong fontHandle) { + const minikin::Font* font = reinterpret_cast<minikin::Font*>(fontHandle); + MinikinFontSkia* minikinSkia = static_cast<MinikinFontSkia*>(font->typeface().get()); + + uint64_t result = font->style().weight(); + result |= font->style().slant() == minikin::FontStyle::Slant::ITALIC ? 0x10000 : 0x00000; + result |= ((static_cast<uint64_t>(minikinSkia->GetFontIndex())) << 32); + result |= ((static_cast<uint64_t>(minikinSkia->GetAxes().size())) << 48); + return result; +} + +// Critical Native +static jlong Font_getAxisInfo(CRITICAL_JNI_PARAMS_COMMA jlong fontHandle, jint index) { + const minikin::Font* font = reinterpret_cast<minikin::Font*>(fontHandle); + MinikinFontSkia* minikinSkia = static_cast<MinikinFontSkia*>(font->typeface().get()); + const minikin::FontVariation& var = minikinSkia->GetAxes().at(index); + uint32_t floatBinary = *reinterpret_cast<const uint32_t*>(&var.value); + return (static_cast<uint64_t>(var.axisTag) << 32) | static_cast<uint64_t>(floatBinary); +} + +// FastNative +static jstring Font_getFontPath(JNIEnv* env, jobject, jlong fontHandle) { + const minikin::Font* font = reinterpret_cast<minikin::Font*>(fontHandle); + MinikinFontSkia* minikinSkia = static_cast<MinikinFontSkia*>(font->typeface().get()); + const std::string& filePath = minikinSkia->getFilePath(); + if (filePath.empty()) { + return nullptr; + } + return env->NewStringUTF(filePath.c_str()); +} + +// Critical Native +static jlong Font_getNativeFontPtr(CRITICAL_JNI_PARAMS_COMMA jlong fontHandle) { + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontHandle); + return reinterpret_cast<jlong>(font->font.get()); +} + +// Critical Native +static jboolean Font_isSameBufferAddress(CRITICAL_JNI_PARAMS_COMMA jlong lFontHandle, + jlong rFontHandle) { + FontWrapper* lFont = reinterpret_cast<FontWrapper*>(lFontHandle); + FontWrapper* rFont = reinterpret_cast<FontWrapper*>(rFontHandle); + const void* lBufferPtr = lFont->font->typeface()->GetFontData(); + const void* rBufferPtr = rFont->font->typeface()->GetFontData(); + return lBufferPtr == rBufferPtr; +} + +/////////////////////////////////////////////////////////////////////////////// + +struct FontBufferWrapper { + FontBufferWrapper(const std::shared_ptr<minikin::MinikinFont>& font) : minikinFont(font) {} + // MinikinFont holds a shared pointer of SkTypeface which has reference to font data. + std::shared_ptr<minikin::MinikinFont> minikinFont; +}; + +static void unrefBuffer(jlong nativePtr) { + FontBufferWrapper* wrapper = reinterpret_cast<FontBufferWrapper*>(nativePtr); + delete wrapper; +} + +// Critical Native +static jlong FontBufferHelper_refFontBuffer(CRITICAL_JNI_PARAMS_COMMA jlong fontHandle) { + const minikin::Font* font = reinterpret_cast<minikin::Font*>(fontHandle); + return reinterpret_cast<jlong>(new FontBufferWrapper(font->typeface())); +} + +// Fast Native +static jobject FontBufferHelper_wrapByteBuffer(JNIEnv* env, jobject, jlong nativePtr) { + FontBufferWrapper* wrapper = reinterpret_cast<FontBufferWrapper*>(nativePtr); + return env->NewDirectByteBuffer( + const_cast<void*>(wrapper->minikinFont->GetFontData()), + wrapper->minikinFont->GetFontSize()); +} + +// Critical Native +static jlong FontBufferHelper_getReleaseFunc(CRITICAL_JNI_PARAMS) { + return reinterpret_cast<jlong>(unrefBuffer); +} + +/////////////////////////////////////////////////////////////////////////////// + static const JNINativeMethod gFontBuilderMethods[] = { { "nInitBuilder", "()J", (void*) Font_Builder_initBuilder }, { "nAddAxis", "(JIF)V", (void*) Font_Builder_addAxis }, { "nBuild", "(JLjava/nio/ByteBuffer;Ljava/lang/String;IZI)J", (void*) Font_Builder_build }, + { "nClone", "(JJIZI)J", (void*) Font_Builder_clone }, { "nGetReleaseNativeFont", "()J", (void*) Font_Builder_getReleaseNativeFont }, }; +static const JNINativeMethod gFontMethods[] = { + { "nGetGlyphBounds", "(JIJLandroid/graphics/RectF;)F", (void*) Font_getGlyphBounds }, + { "nGetFontMetrics", "(JJLandroid/graphics/Paint$FontMetrics;)F", (void*) Font_getFontMetrics }, + { "nGetFontInfo", "(J)J", (void*) Font_getFontInfo }, + { "nGetAxisInfo", "(JI)J", (void*) Font_getAxisInfo }, + { "nGetFontPath", "(J)Ljava/lang/String;", (void*) Font_getFontPath }, + { "nGetNativeFontPtr", "(J)J", (void*) Font_getNativeFontPtr }, + { "nIsSameBufferAddress", "(JJ)Z", (void*) Font_isSameBufferAddress }, +}; + +static const JNINativeMethod gFontBufferHelperMethods[] = { + { "nRefFontBuffer", "(J)J", (void*) FontBufferHelper_refFontBuffer }, + { "nWrapByteBuffer", "(J)Ljava/nio/ByteBuffer;", (void*) FontBufferHelper_wrapByteBuffer }, + { "nGetReleaseFunc", "()J", (void*) FontBufferHelper_getReleaseFunc }, +}; + int register_android_graphics_fonts_Font(JNIEnv* env) { return RegisterMethodsOrDie(env, "android/graphics/fonts/Font$Builder", gFontBuilderMethods, - NELEM(gFontBuilderMethods)); + NELEM(gFontBuilderMethods)) + + RegisterMethodsOrDie(env, "android/graphics/fonts/Font", gFontMethods, + NELEM(gFontMethods)) + + RegisterMethodsOrDie(env, "android/graphics/fonts/NativeFontBufferHelper", + gFontBufferHelperMethods, NELEM(gFontBufferHelperMethods)); } } diff --git a/libs/hwui/jni/fonts/FontFamily.cpp b/libs/hwui/jni/fonts/FontFamily.cpp index df619d9f1406..37e52766f2ef 100644 --- a/libs/hwui/jni/fonts/FontFamily.cpp +++ b/libs/hwui/jni/fonts/FontFamily.cpp @@ -30,7 +30,7 @@ namespace android { struct NativeFamilyBuilder { - std::vector<minikin::Font> fonts; + std::vector<std::shared_ptr<minikin::Font>> fonts; }; static inline NativeFamilyBuilder* toBuilder(jlong ptr) { diff --git a/libs/hwui/jni/text/TextShaper.cpp b/libs/hwui/jni/text/TextShaper.cpp new file mode 100644 index 000000000000..9785aa537f65 --- /dev/null +++ b/libs/hwui/jni/text/TextShaper.cpp @@ -0,0 +1,206 @@ +/* + * 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. + */ + +#undef LOG_TAG +#define LOG_TAG "TextShaper" + +#include "graphics_jni_helpers.h" +#include <nativehelper/ScopedStringChars.h> +#include <nativehelper/ScopedPrimitiveArray.h> +#include <set> +#include <algorithm> + +#include "SkPaint.h" +#include "SkTypeface.h" +#include <hwui/MinikinSkia.h> +#include <hwui/MinikinUtils.h> +#include <hwui/Paint.h> +#include <minikin/MinikinPaint.h> +#include <minikin/MinikinFont.h> + +namespace android { + +struct LayoutWrapper { + LayoutWrapper(minikin::Layout&& layout, float ascent, float descent) + : layout(std::move(layout)), ascent(ascent), descent(descent) {} + minikin::Layout layout; + float ascent; + float descent; +}; + +static void releaseLayout(jlong ptr) { + delete reinterpret_cast<LayoutWrapper*>(ptr); +} + +static jlong shapeTextRun(const uint16_t* text, int textSize, int start, int count, + int contextStart, int contextCount, minikin::Bidi bidiFlags, + const Paint& paint, const Typeface* typeface) { + + minikin::MinikinPaint minikinPaint = MinikinUtils::prepareMinikinPaint(&paint, typeface); + + minikin::Layout layout = MinikinUtils::doLayout(&paint, bidiFlags, typeface, + text, textSize, start, count, contextStart, contextCount, nullptr); + + std::set<const minikin::Font*> seenFonts; + float overallAscent = 0; + float overallDescent = 0; + for (int i = 0; i < layout.nGlyphs(); ++i) { + const minikin::Font* font = layout.getFont(i); + if (seenFonts.find(font) != seenFonts.end()) continue; + minikin::MinikinExtent extent = {}; + font->typeface()->GetFontExtent(&extent, minikinPaint, layout.getFakery(i)); + overallAscent = std::min(overallAscent, extent.ascent); + overallDescent = std::max(overallDescent, extent.descent); + } + + std::unique_ptr<LayoutWrapper> ptr = std::make_unique<LayoutWrapper>( + std::move(layout), overallAscent, overallDescent + ); + + return reinterpret_cast<jlong>(ptr.release()); +} + +static jlong TextShaper_shapeTextRunChars(JNIEnv *env, jobject, jcharArray charArray, + jint start, jint count, jint contextStart, jint contextCount, jboolean isRtl, + jlong paintPtr) { + ScopedCharArrayRO text(env, charArray); + Paint* paint = reinterpret_cast<Paint*>(paintPtr); + const Typeface* typeface = paint->getAndroidTypeface(); + const minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR; + return shapeTextRun( + text.get(), text.size(), + start, count, + contextStart, contextCount, + bidiFlags, + *paint, typeface); + +} + +static jlong TextShaper_shapeTextRunString(JNIEnv *env, jobject, jstring string, + jint start, jint count, jint contextStart, jint contextCount, jboolean isRtl, + jlong paintPtr) { + ScopedStringChars text(env, string); + Paint* paint = reinterpret_cast<Paint*>(paintPtr); + const Typeface* typeface = paint->getAndroidTypeface(); + const minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR; + return shapeTextRun( + text.get(), text.size(), + start, count, + contextStart, contextCount, + bidiFlags, + *paint, typeface); +} + +// CriticalNative +static jint TextShaper_Result_getGlyphCount(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { + const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); + return layout->layout.nGlyphs(); +} + +// CriticalNative +static jfloat TextShaper_Result_getTotalAdvance(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { + const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); + return layout->layout.getAdvance(); +} + +// CriticalNative +static jfloat TextShaper_Result_getAscent(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { + const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); + return layout->ascent; +} + +// CriticalNative +static jfloat TextShaper_Result_getDescent(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { + const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); + return layout->descent; +} + +// CriticalNative +static jint TextShaper_Result_getGlyphId(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) { + const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); + return layout->layout.getGlyphId(i); +} + +// CriticalNative +static jfloat TextShaper_Result_getX(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) { + const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); + return layout->layout.getX(i); +} + +// CriticalNative +static jfloat TextShaper_Result_getY(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) { + const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); + return layout->layout.getY(i); +} + +// CriticalNative +static jlong TextShaper_Result_getFont(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) { + const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); + return reinterpret_cast<jlong>(layout->layout.getFont(i)); +} + +// CriticalNative +static jlong TextShaper_Result_nReleaseFunc(CRITICAL_JNI_PARAMS) { + return reinterpret_cast<jlong>(releaseLayout); +} + +static const JNINativeMethod gMethods[] = { + // Fast Natives + {"nativeShapeTextRun", "(" + "[C" // text + "I" // start + "I" // count + "I" // contextStart + "I" // contextCount + "Z" // isRtl + "J)" // paint + "J", // LayoutPtr + (void*) TextShaper_shapeTextRunChars}, + + {"nativeShapeTextRun", "(" + "Ljava/lang/String;" // text + "I" // start + "I" // count + "I" // contextStart + "I" // contextCount + "Z" // isRtl + "J)" // paint + "J", // LayoutPtr + (void*) TextShaper_shapeTextRunString}, + +}; + +static const JNINativeMethod gResultMethods[] = { + { "nGetGlyphCount", "(J)I", (void*)TextShaper_Result_getGlyphCount }, + { "nGetTotalAdvance", "(J)F", (void*)TextShaper_Result_getTotalAdvance }, + { "nGetAscent", "(J)F", (void*)TextShaper_Result_getAscent }, + { "nGetDescent", "(J)F", (void*)TextShaper_Result_getDescent }, + { "nGetGlyphId", "(JI)I", (void*)TextShaper_Result_getGlyphId }, + { "nGetX", "(JI)F", (void*)TextShaper_Result_getX }, + { "nGetY", "(JI)F", (void*)TextShaper_Result_getY }, + { "nGetFont", "(JI)J", (void*)TextShaper_Result_getFont }, + { "nReleaseFunc", "()J", (void*)TextShaper_Result_nReleaseFunc }, +}; + +int register_android_graphics_text_TextShaper(JNIEnv* env) { + return RegisterMethodsOrDie(env, "android/graphics/text/TextRunShaper", gMethods, + NELEM(gMethods)) + + RegisterMethodsOrDie(env, "android/graphics/text/PositionedGlyphs", + gResultMethods, NELEM(gResultMethods)); +} + +} + diff --git a/libs/hwui/libhwui.map.txt b/libs/hwui/libhwui.map.txt new file mode 100644 index 000000000000..73de0d12a60b --- /dev/null +++ b/libs/hwui/libhwui.map.txt @@ -0,0 +1,70 @@ +LIBHWUI { + global: + /* listing of all C APIs to be exposed by libhwui to consumers outside of the module */ + ABitmap_getInfoFromJava; + ABitmap_acquireBitmapFromJava; + ABitmap_copy; + ABitmap_acquireRef; + ABitmap_releaseRef; + ABitmap_getInfo; + ABitmap_getDataSpace; + ABitmap_getPixels; + ABitmap_notifyPixelsChanged; + ABitmapConfig_getFormatFromConfig; + ABitmapConfig_getConfigFromFormat; + ABitmap_compress; + ABitmap_getHardwareBuffer; + ACanvas_isSupportedPixelFormat; + ACanvas_getNativeHandleFromJava; + ACanvas_createCanvas; + ACanvas_destroyCanvas; + ACanvas_setBuffer; + ACanvas_clipRect; + ACanvas_clipOutRect; + ACanvas_drawRect; + ACanvas_drawBitmap; + init_android_graphics; + register_android_graphics_classes; + register_android_graphics_GraphicsStatsService; + zygote_preload_graphics; + AMatrix_getContents; + APaint_createPaint; + APaint_destroyPaint; + APaint_setBlendMode; + ARegionIterator_acquireIterator; + ARegionIterator_releaseIterator; + ARegionIterator_isComplex; + ARegionIterator_isDone; + ARegionIterator_next; + ARegionIterator_getRect; + ARegionIterator_getTotalBounds; + ARenderThread_dumpGraphicsMemory; + local: + *; +}; + +LIBHWUI_PLATFORM { + global: + extern "C++" { + /* required by libwebviewchromium_plat_support */ + android::uirenderer::ColorSpaceToADataSpace*; + android::uirenderer::WebViewFunctor_*; + GraphicsJNI::getNativeCanvas*; + SkCanvasStateUtils::ReleaseCanvasState*; + SkColorSpace::toXYZD50*; + SkColorSpace::transferFn*; + /* required by libjnigraphics */ + android::ImageDecoder::*; + android::uirenderer::DataSpaceToColorSpace*; + android::uirenderer::ColorSpaceToADataSpace*; + getMimeType*; + SkAndroidCodec::*; + SkCodec::MakeFromStream*; + SkColorInfo::*; + SkFILEStream::SkFILEStream*; + SkImageInfo::*; + SkMemoryStream::SkMemoryStream*; + }; + local: + *; +}; diff --git a/libs/hwui/pipeline/skia/FunctorDrawable.h b/libs/hwui/pipeline/skia/FunctorDrawable.h index cf2f93b95e71..988a896b6267 100644 --- a/libs/hwui/pipeline/skia/FunctorDrawable.h +++ b/libs/hwui/pipeline/skia/FunctorDrawable.h @@ -16,8 +16,6 @@ #pragma once -#include "GlFunctorLifecycleListener.h" - #include <SkCanvas.h> #include <SkDrawable.h> @@ -36,44 +34,21 @@ namespace skiapipeline { */ class FunctorDrawable : public SkDrawable { public: - FunctorDrawable(Functor* functor, GlFunctorLifecycleListener* listener, SkCanvas* canvas) - : mBounds(canvas->getLocalClipBounds()) - , mAnyFunctor(std::in_place_type<LegacyFunctor>, functor, listener) {} - FunctorDrawable(int functor, SkCanvas* canvas) : mBounds(canvas->getLocalClipBounds()) - , mAnyFunctor(std::in_place_type<NewFunctor>, functor) {} + , mWebViewHandle(WebViewFunctorManager::instance().handleFor(functor)) {} virtual ~FunctorDrawable() {} virtual void syncFunctor(const WebViewSyncData& data) const { - if (mAnyFunctor.index() == 0) { - std::get<0>(mAnyFunctor).handle->sync(data); - } else { - (*(std::get<1>(mAnyFunctor).functor))(DrawGlInfo::kModeSync, nullptr); - } + mWebViewHandle->sync(data); } protected: virtual SkRect onGetBounds() override { return mBounds; } const SkRect mBounds; - - struct LegacyFunctor { - explicit LegacyFunctor(Functor* functor, GlFunctorLifecycleListener* listener) - : functor(functor), listener(listener) {} - Functor* functor; - sp<GlFunctorLifecycleListener> listener; - }; - - struct NewFunctor { - explicit NewFunctor(int functor) { - handle = WebViewFunctorManager::instance().handleFor(functor); - } - sp<WebViewFunctor::Handle> handle; - }; - - std::variant<NewFunctor, LegacyFunctor> mAnyFunctor; + sp<WebViewFunctor::Handle> mWebViewHandle; }; } // namespace skiapipeline diff --git a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp index 8f67f97fb4bc..bfbdc5c009c0 100644 --- a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp @@ -15,10 +15,9 @@ */ #include "GLFunctorDrawable.h" -#include <GrContext.h> +#include <GrDirectContext.h> #include <private/hwui/DrawGlInfo.h> #include "FunctorDrawable.h" -#include "GlFunctorLifecycleListener.h" #include "GrBackendSurface.h" #include "GrRenderTarget.h" #include "GrRenderTargetContext.h" @@ -26,20 +25,12 @@ #include "SkAndroidFrameworkUtils.h" #include "SkClipStack.h" #include "SkRect.h" -#include "include/private/SkM44.h" +#include "SkM44.h" namespace android { namespace uirenderer { namespace skiapipeline { -GLFunctorDrawable::~GLFunctorDrawable() { - if (auto lp = std::get_if<LegacyFunctor>(&mAnyFunctor)) { - if (lp->listener) { - lp->listener->onGlFunctorReleased(lp->functor); - } - } -} - static void setScissor(int viewportHeight, const SkIRect& clip) { SkASSERT(!clip.isEmpty()); // transform to Y-flipped GL space, and prevent negatives @@ -65,7 +56,8 @@ static void GetFboDetails(SkCanvas* canvas, GLuint* outFboID, SkISize* outFboSiz } void GLFunctorDrawable::onDraw(SkCanvas* canvas) { - if (canvas->getGrContext() == nullptr) { + GrDirectContext* directContext = GrAsDirectContext(canvas->recordingContext()); + if (directContext == nullptr) { // We're dumping a picture, render a light-blue rectangle instead // TODO: Draw the WebView text on top? Seemingly complicated as SkPaint doesn't // seem to have a default typeface that works. We only ever use drawGlyphs, which @@ -85,7 +77,7 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) { SkIRect surfaceBounds = canvas->internal_private_getTopLayerBounds(); SkIRect clipBounds = canvas->getDeviceClipBounds(); - SkM44 mat4(canvas->experimental_getLocalToDevice()); + SkM44 mat4(canvas->getLocalToDevice()); SkRegion clipRegion; canvas->temporary_internal_getRgnClip(&clipRegion); @@ -96,7 +88,7 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) { SkImageInfo surfaceInfo = canvas->imageInfo().makeWH(clipBounds.width(), clipBounds.height()); tmpSurface = - SkSurface::MakeRenderTarget(canvas->getGrContext(), SkBudgeted::kYes, surfaceInfo); + SkSurface::MakeRenderTarget(directContext, SkBudgeted::kYes, surfaceInfo); tmpSurface->getCanvas()->clear(SK_ColorTRANSPARENT); GrGLFramebufferInfo fboInfo; @@ -150,7 +142,7 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) { // notify Skia that we just updated the FBO and stencil const uint32_t grState = kStencil_GrGLBackendState | kRenderTarget_GrGLBackendState; - canvas->getGrContext()->resetContext(grState); + directContext->resetContext(grState); SkCanvas* tmpCanvas = canvas; if (tmpSurface) { @@ -186,11 +178,7 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) { setScissor(info.height, clipRegion.getBounds()); } - if (mAnyFunctor.index() == 0) { - std::get<0>(mAnyFunctor).handle->drawGl(info); - } else { - (*(std::get<1>(mAnyFunctor).functor))(DrawGlInfo::kModeDraw, &info); - } + mWebViewHandle->drawGl(info); if (clearStencilAfterFunctor) { // clear stencil buffer as it may be used by Skia @@ -201,7 +189,7 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) { glClear(GL_STENCIL_BUFFER_BIT); } - canvas->getGrContext()->resetContext(); + directContext->resetContext(); // if there were unclipped save layers involved we draw our offscreen surface to the canvas if (tmpSurface) { diff --git a/libs/hwui/pipeline/skia/GLFunctorDrawable.h b/libs/hwui/pipeline/skia/GLFunctorDrawable.h index 2ea4f67428bc..4092e8dfa3a5 100644 --- a/libs/hwui/pipeline/skia/GLFunctorDrawable.h +++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.h @@ -33,7 +33,7 @@ class GLFunctorDrawable : public FunctorDrawable { public: using FunctorDrawable::FunctorDrawable; - virtual ~GLFunctorDrawable(); + virtual ~GLFunctorDrawable() {} protected: void onDraw(SkCanvas* canvas) override; diff --git a/libs/hwui/pipeline/skia/LayerDrawable.cpp b/libs/hwui/pipeline/skia/LayerDrawable.cpp index f839213e9007..f95f347cffaf 100644 --- a/libs/hwui/pipeline/skia/LayerDrawable.cpp +++ b/libs/hwui/pipeline/skia/LayerDrawable.cpp @@ -29,7 +29,7 @@ namespace skiapipeline { void LayerDrawable::onDraw(SkCanvas* canvas) { Layer* layer = mLayerUpdater->backingLayer(); if (layer) { - DrawLayer(canvas->getGrContext(), canvas, layer, nullptr, nullptr, true); + DrawLayer(canvas->recordingContext(), canvas, layer, nullptr, nullptr, true); } } @@ -67,8 +67,12 @@ static bool shouldFilterRect(const SkMatrix& matrix, const SkRect& srcRect, cons isIntegerAligned(dstDevRect.y())); } -bool LayerDrawable::DrawLayer(GrContext* context, SkCanvas* canvas, Layer* layer, - const SkRect* srcRect, const SkRect* dstRect, +// TODO: Context arg probably doesn't belong here – do debug check at callsite instead. +bool LayerDrawable::DrawLayer(GrRecordingContext* context, + SkCanvas* canvas, + Layer* layer, + const SkRect* srcRect, + const SkRect* dstRect, bool useLayerTransform) { if (context == nullptr) { SkDEBUGF(("Attempting to draw LayerDrawable into an unsupported surface")); diff --git a/libs/hwui/pipeline/skia/LayerDrawable.h b/libs/hwui/pipeline/skia/LayerDrawable.h index 7cd515ae9fcb..ffbb480023ac 100644 --- a/libs/hwui/pipeline/skia/LayerDrawable.h +++ b/libs/hwui/pipeline/skia/LayerDrawable.h @@ -32,8 +32,12 @@ class LayerDrawable : public SkDrawable { public: explicit LayerDrawable(DeferredLayerUpdater* layerUpdater) : mLayerUpdater(layerUpdater) {} - static bool DrawLayer(GrContext* context, SkCanvas* canvas, Layer* layer, const SkRect* srcRect, - const SkRect* dstRect, bool useLayerTransform); + static bool DrawLayer(GrRecordingContext* context, + SkCanvas* canvas, + Layer* layer, + const SkRect* srcRect, + const SkRect* dstRect, + bool useLayerTransform); protected: virtual SkRect onGetBounds() override { diff --git a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp index 00ceb2d84f9e..1473b3e5abb7 100644 --- a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp +++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp @@ -172,10 +172,12 @@ static bool layerNeedsPaint(const LayerProperties& properties, float alphaMultip SkPaint* paint) { paint->setFilterQuality(kLow_SkFilterQuality); if (alphaMultiplier < 1.0f || properties.alpha() < 255 || - properties.xferMode() != SkBlendMode::kSrcOver || properties.getColorFilter() != nullptr) { + properties.xferMode() != SkBlendMode::kSrcOver || properties.getColorFilter() != nullptr || + properties.getImageFilter() != nullptr) { paint->setAlpha(properties.alpha() * alphaMultiplier); paint->setBlendMode(properties.xferMode()); paint->setColorFilter(sk_ref_sp(properties.getColorFilter())); + paint->setImageFilter(sk_ref_sp(properties.getImageFilter())); return true; } return false; diff --git a/libs/hwui/pipeline/skia/ShaderCache.cpp b/libs/hwui/pipeline/skia/ShaderCache.cpp index 66aa8c203799..3baff7ea8f90 100644 --- a/libs/hwui/pipeline/skia/ShaderCache.cpp +++ b/libs/hwui/pipeline/skia/ShaderCache.cpp @@ -15,7 +15,7 @@ */ #include "ShaderCache.h" -#include <GrContext.h> +#include <GrDirectContext.h> #include <log/log.h> #include <openssl/sha.h> #include <algorithm> @@ -206,7 +206,7 @@ void ShaderCache::store(const SkData& key, const SkData& data) { } } -void ShaderCache::onVkFrameFlushed(GrContext* context) { +void ShaderCache::onVkFrameFlushed(GrDirectContext* context) { { std::lock_guard<std::mutex> lock(mMutex); diff --git a/libs/hwui/pipeline/skia/ShaderCache.h b/libs/hwui/pipeline/skia/ShaderCache.h index 0898017d52a1..4dcc9fb49802 100644 --- a/libs/hwui/pipeline/skia/ShaderCache.h +++ b/libs/hwui/pipeline/skia/ShaderCache.h @@ -37,7 +37,7 @@ public: * "get" returns a pointer to the singleton ShaderCache object. This * singleton object will never be destroyed. */ - ANDROID_API static ShaderCache& get(); + static ShaderCache& get(); /** * initShaderDiskCache" loads the serialized cache contents from disk, @@ -80,7 +80,7 @@ public: * Pipeline cache is saved on disk only if the size of the data has changed or there was * a new shader compiled. */ - void onVkFrameFlushed(GrContext* context); + void onVkFrameFlushed(GrDirectContext* context); private: // Creation and (the lack of) destruction is handled internally. diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp index 24a6228242a5..389fe7eed7c7 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp @@ -87,6 +87,8 @@ bool SkiaOpenGLPipeline::draw(const Frame& frame, const SkRect& screenDirty, con // Note: The default preference of pixel format is RGBA_8888, when other // pixel format is available, we should branch out and do more check. fboInfo.fFormat = GL_RGBA8; + } else if (colorType == kRGBA_1010102_SkColorType) { + fboInfo.fFormat = GL_RGB10_A2; } else { LOG_ALWAYS_FATAL("Unsupported color type."); } diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index 5088494d6a07..6e7493cb443d 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -35,6 +35,7 @@ #include "VectorDrawable.h" #include "thread/CommonPool.h" #include "tools/SkSharingProc.h" +#include "utils/Color.h" #include "utils/String8.h" #include "utils/TraceUtils.h" @@ -85,7 +86,7 @@ void SkiaPipeline::renderLayers(const LightGeometry& lightGeometry, } void SkiaPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) { - sk_sp<GrContext> cachedContext; + sk_sp<GrDirectContext> cachedContext; // Render all layers that need to be updated, in order. for (size_t i = 0; i < layers.entries().size(); i++) { @@ -141,11 +142,12 @@ void SkiaPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) // cache the current context so that we can defer flushing it until // either all the layers have been rendered or the context changes - GrContext* currentContext = layerNode->getLayerSurface()->getCanvas()->getGrContext(); + GrDirectContext* currentContext = + GrAsDirectContext(layerNode->getLayerSurface()->getCanvas()->recordingContext()); if (cachedContext.get() != currentContext) { if (cachedContext.get()) { ATRACE_NAME("flush layers (context changed)"); - cachedContext->flush(); + cachedContext->flushAndSubmit(); } cachedContext.reset(SkSafeRef(currentContext)); } @@ -153,7 +155,7 @@ void SkiaPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) if (cachedContext.get()) { ATRACE_NAME("flush layers"); - cachedContext->flush(); + cachedContext->flushAndSubmit(); } } @@ -200,7 +202,7 @@ bool SkiaPipeline::createOrUpdateLayer(RenderNode* node, const DamageAccumulator } void SkiaPipeline::prepareToDraw(const RenderThread& thread, Bitmap* bitmap) { - GrContext* context = thread.getGrContext(); + GrDirectContext* context = thread.getGrContext(); if (context) { ATRACE_FORMAT("Bitmap#prepareToDraw %dx%d", bitmap->width(), bitmap->height()); auto image = bitmap->makeImage(); @@ -450,7 +452,7 @@ void SkiaPipeline::renderFrame(const LayerUpdateQueue& layers, const SkRect& cli } ATRACE_NAME("flush commands"); - surface->getCanvas()->flush(); + surface->flushAndSubmit(); Properties::skpCaptureEnabled = previousSkpEnabled; } @@ -587,14 +589,23 @@ void SkiaPipeline::dumpResourceCacheUsage() const { void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) { mColorMode = colorMode; - if (colorMode == ColorMode::SRGB) { - mSurfaceColorType = SkColorType::kN32_SkColorType; - mSurfaceColorSpace = SkColorSpace::MakeSRGB(); - } else if (colorMode == ColorMode::WideColorGamut) { - mSurfaceColorType = DeviceInfo::get()->getWideColorType(); - mSurfaceColorSpace = DeviceInfo::get()->getWideColorSpace(); - } else { - LOG_ALWAYS_FATAL("Unreachable: unsupported color mode."); + switch (colorMode) { + case ColorMode::Default: + mSurfaceColorType = SkColorType::kN32_SkColorType; + mSurfaceColorSpace = SkColorSpace::MakeSRGB(); + break; + case ColorMode::WideColorGamut: + mSurfaceColorType = DeviceInfo::get()->getWideColorType(); + mSurfaceColorSpace = DeviceInfo::get()->getWideColorSpace(); + break; + case ColorMode::Hdr: + mSurfaceColorType = SkColorType::kRGBA_F16_SkColorType; + mSurfaceColorSpace = SkColorSpace::MakeRGB(GetPQSkTransferFunction(), SkNamedGamut::kRec2020); + break; + case ColorMode::Hdr10: + mSurfaceColorType = SkColorType::kRGBA_1010102_SkColorType; + mSurfaceColorSpace = SkColorSpace::MakeRGB(GetPQSkTransferFunction(), SkNamedGamut::kRec2020); + break; } } diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h index 8341164edc19..100bfb6b159a 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaPipeline.h @@ -50,7 +50,7 @@ public: bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator, ErrorHandler* errorHandler) override; - void setSurfaceColorProperties(renderthread::ColorMode colorMode) override; + void setSurfaceColorProperties(ColorMode colorMode) override; SkColorType getSurfaceColorType() const override { return mSurfaceColorType; } sk_sp<SkColorSpace> getSurfaceColorSpace() override { return mSurfaceColorSpace; } @@ -76,7 +76,7 @@ protected: renderthread::RenderThread& mRenderThread; - renderthread::ColorMode mColorMode = renderthread::ColorMode::SRGB; + ColorMode mColorMode = ColorMode::Default; SkColorType mSurfaceColorType; sk_sp<SkColorSpace> mSurfaceColorSpace; diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp index d67cf8c9c73f..e292cbdd101f 100644 --- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp +++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp @@ -57,7 +57,7 @@ void SkiaRecordingCanvas::initDisplayList(uirenderer::RenderNode* renderNode, in uirenderer::DisplayList* SkiaRecordingCanvas::finishRecording() { // close any existing chunks if necessary - insertReorderBarrier(false); + enableZ(false); mRecorder.restoreToCount(1); return mDisplayList.release(); } @@ -85,8 +85,8 @@ void SkiaRecordingCanvas::drawCircle(uirenderer::CanvasPropertyPrimitive* x, drawDrawable(mDisplayList->allocateDrawable<AnimatedCircle>(x, y, radius, paint)); } -void SkiaRecordingCanvas::insertReorderBarrier(bool enableReorder) { - if (mCurrentBarrier && enableReorder) { +void SkiaRecordingCanvas::enableZ(bool enableZ) { + if (mCurrentBarrier && enableZ) { // Already in a re-order section, nothing to do return; } @@ -98,7 +98,7 @@ void SkiaRecordingCanvas::insertReorderBarrier(bool enableReorder) { mCurrentBarrier = nullptr; drawDrawable(drawable); } - if (enableReorder) { + if (enableZ) { mCurrentBarrier = mDisplayList->allocateDrawable<StartReorderBarrierDrawable>(mDisplayList.get()); drawDrawable(mCurrentBarrier); @@ -132,23 +132,6 @@ void SkiaRecordingCanvas::drawRenderNode(uirenderer::RenderNode* renderNode) { } } - -void SkiaRecordingCanvas::callDrawGLFunction(Functor* functor, - uirenderer::GlFunctorLifecycleListener* listener) { -#ifdef __ANDROID__ // Layoutlib does not support GL, Vulcan etc. - FunctorDrawable* functorDrawable; - if (Properties::getRenderPipelineType() == RenderPipelineType::SkiaVulkan) { - functorDrawable = mDisplayList->allocateDrawable<VkInteropFunctorDrawable>( - functor, listener, asSkCanvas()); - } else { - functorDrawable = - mDisplayList->allocateDrawable<GLFunctorDrawable>(functor, listener, asSkCanvas()); - } - mDisplayList->mChildFunctors.push_back(functorDrawable); - drawDrawable(functorDrawable); -#endif -} - void SkiaRecordingCanvas::drawWebViewFunctor(int functor) { #ifdef __ANDROID__ // Layoutlib does not support GL, Vulcan etc. FunctorDrawable* functorDrawable; diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h index bd5274c94e75..83e934974afd 100644 --- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h +++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h @@ -69,11 +69,10 @@ public: virtual void drawVectorDrawable(VectorDrawableRoot* vectorDrawable) override; - virtual void insertReorderBarrier(bool enableReorder) override; + virtual void enableZ(bool enableZ) override; virtual void drawLayer(uirenderer::DeferredLayerUpdater* layerHandle) override; virtual void drawRenderNode(uirenderer::RenderNode* renderNode) override; - virtual void callDrawGLFunction(Functor* functor, - uirenderer::GlFunctorLifecycleListener* listener) override; + void drawWebViewFunctor(int functor) override; private: diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp index 212a4284a824..ad6363b4452d 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp @@ -29,7 +29,7 @@ #include <SkSurface.h> #include <SkTypes.h> -#include <GrContext.h> +#include <GrDirectContext.h> #include <GrTypes.h> #include <vk/GrVkTypes.h> diff --git a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp index 68f111752a4c..6efe1762976b 100644 --- a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp @@ -20,7 +20,7 @@ #include <GrBackendDrawableInfo.h> #include <SkAndroidFrameworkUtils.h> #include <SkImage.h> -#include "include/private/SkM44.h" +#include <SkM44.h> #include <utils/Color.h> #include <utils/Trace.h> #include <utils/TraceUtils.h> @@ -96,7 +96,7 @@ void VkFunctorDrawable::onDraw(SkCanvas* canvas) { // "VkFunctorDrawable::onDraw" is not invoked for the most common case, when drawing in a GPU // canvas. - if (canvas->getGrContext() == nullptr) { + if (canvas->recordingContext() == nullptr) { // We're dumping a picture, render a light-blue rectangle instead SkPaint paint; paint.setColor(0xFF81D4FA); @@ -121,12 +121,7 @@ std::unique_ptr<FunctorDrawable::GpuDrawHandler> VkFunctorDrawable::onSnapGpuDra return nullptr; } std::unique_ptr<VkFunctorDrawHandler> draw; - if (mAnyFunctor.index() == 0) { - return std::make_unique<VkFunctorDrawHandler>(std::get<0>(mAnyFunctor).handle, matrix, clip, - image_info); - } else { - LOG_ALWAYS_FATAL("Not implemented"); - } + return std::make_unique<VkFunctorDrawHandler>(mWebViewHandle, matrix, clip, image_info); } } // namespace skiapipeline diff --git a/libs/hwui/pipeline/skia/VkFunctorDrawable.h b/libs/hwui/pipeline/skia/VkFunctorDrawable.h index d3f97773b91d..fbfc6e76595e 100644 --- a/libs/hwui/pipeline/skia/VkFunctorDrawable.h +++ b/libs/hwui/pipeline/skia/VkFunctorDrawable.h @@ -19,7 +19,6 @@ #include "FunctorDrawable.h" #include <SkImageInfo.h> -#include <ui/GraphicBuffer.h> #include <utils/RefBase.h> namespace android { diff --git a/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp b/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp index 241d3708def5..bc8ce428ce2e 100644 --- a/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp @@ -15,23 +15,24 @@ */ #include "VkInteropFunctorDrawable.h" -#include <private/hwui/DrawGlInfo.h> +#include <EGL/egl.h> +#include <EGL/eglext.h> +#include <GLES2/gl2.h> +#include <GLES2/gl2ext.h> +#include <GLES3/gl3.h> +#include <private/hwui/DrawGlInfo.h> #include <utils/Color.h> +#include <utils/GLUtils.h> #include <utils/Trace.h> #include <utils/TraceUtils.h> + #include <thread> + #include "renderthread/EglManager.h" #include "thread/ThreadBase.h" #include "utils/TimeUtils.h" -#include <EGL/eglext.h> -#include <GLES2/gl2.h> -#include <GLES2/gl2ext.h> -#include <GLES3/gl3.h> - -#include <utils/GLUtils.h> - namespace android { namespace uirenderer { namespace skiapipeline { @@ -66,7 +67,7 @@ void VkInteropFunctorDrawable::vkInvokeFunctor(Functor* functor) { void VkInteropFunctorDrawable::onDraw(SkCanvas* canvas) { ATRACE_CALL(); - if (canvas->getGrContext() == nullptr) { + if (canvas->recordingContext() == nullptr) { SkDEBUGF(("Attempting to draw VkInteropFunctor into an unsupported surface")); return; } @@ -75,20 +76,23 @@ void VkInteropFunctorDrawable::onDraw(SkCanvas* canvas) { SkImageInfo surfaceInfo = canvas->imageInfo(); - if (!mFrameBuffer.get() || mFBInfo != surfaceInfo) { + if (mFrameBuffer == nullptr || mFBInfo != surfaceInfo) { // Buffer will be used as an OpenGL ES render target. - mFrameBuffer = new GraphicBuffer( - // TODO: try to reduce the size of the buffer: possibly by using clip bounds. - static_cast<uint32_t>(surfaceInfo.width()), - static_cast<uint32_t>(surfaceInfo.height()), - ColorTypeToPixelFormat(surfaceInfo.colorType()), - GraphicBuffer::USAGE_HW_TEXTURE | GraphicBuffer::USAGE_SW_WRITE_NEVER | - GraphicBuffer::USAGE_SW_READ_NEVER | GraphicBuffer::USAGE_HW_RENDER, - std::string("VkInteropFunctorDrawable::onDraw pid [") + std::to_string(getpid()) + - "]"); - status_t error = mFrameBuffer->initCheck(); - if (error < 0) { - ALOGW("VkInteropFunctorDrawable::onDraw() failed in GraphicBuffer.create()"); + AHardwareBuffer_Desc desc = { + .width = static_cast<uint32_t>(surfaceInfo.width()), + .height = static_cast<uint32_t>(surfaceInfo.height()), + .layers = 1, + .format = ColorTypeToBufferFormat(surfaceInfo.colorType()), + .usage = AHARDWAREBUFFER_USAGE_CPU_READ_NEVER | + AHARDWAREBUFFER_USAGE_CPU_WRITE_NEVER | + AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | + AHARDWAREBUFFER_USAGE_GPU_FRAMEBUFFER, + }; + + mFrameBuffer = allocateAHardwareBuffer(desc); + + if (!mFrameBuffer) { + ALOGW("VkInteropFunctorDrawable::onDraw() failed in AHardwareBuffer_allocate()"); return; } @@ -106,7 +110,7 @@ void VkInteropFunctorDrawable::onDraw(SkCanvas* canvas) { uirenderer::renderthread::EglManager::eglErrorString()); // We use an EGLImage to access the content of the GraphicBuffer // The EGL image is later bound to a 2D texture - EGLClientBuffer clientBuffer = (EGLClientBuffer)mFrameBuffer->getNativeBuffer(); + const EGLClientBuffer clientBuffer = eglGetNativeClientBufferANDROID(mFrameBuffer.get()); AutoEglImage autoImage(display, clientBuffer); if (autoImage.image == EGL_NO_IMAGE_KHR) { ALOGW("Could not create EGL image, err =%s", @@ -121,7 +125,7 @@ void VkInteropFunctorDrawable::onDraw(SkCanvas* canvas) { glBindTexture(GL_TEXTURE_2D, 0); DrawGlInfo info; - SkM44 mat4(canvas->experimental_getLocalToDevice()); + SkM44 mat4(canvas->getLocalToDevice()); SkIRect clipBounds = canvas->getDeviceClipBounds(); info.clipLeft = clipBounds.fLeft; @@ -151,11 +155,7 @@ void VkInteropFunctorDrawable::onDraw(SkCanvas* canvas) { glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT); - if (mAnyFunctor.index() == 0) { - std::get<0>(mAnyFunctor).handle->drawGl(info); - } else { - (*(std::get<1>(mAnyFunctor).functor))(DrawGlInfo::kModeDraw, &info); - } + mWebViewHandle->drawGl(info); EGLSyncKHR glDrawFinishedFence = eglCreateSyncKHR(eglGetCurrentDisplay(), EGL_SYNC_FENCE_KHR, NULL); @@ -179,22 +179,13 @@ void VkInteropFunctorDrawable::onDraw(SkCanvas* canvas) { // drawing into the offscreen surface, so we need to reset it here. canvas->resetMatrix(); - auto functorImage = SkImage::MakeFromAHardwareBuffer( - reinterpret_cast<AHardwareBuffer*>(mFrameBuffer.get()), kPremul_SkAlphaType, - canvas->imageInfo().refColorSpace(), kBottomLeft_GrSurfaceOrigin); + auto functorImage = SkImage::MakeFromAHardwareBuffer(mFrameBuffer.get(), kPremul_SkAlphaType, + canvas->imageInfo().refColorSpace(), + kBottomLeft_GrSurfaceOrigin); canvas->drawImage(functorImage, 0, 0, &paint); canvas->restore(); } -VkInteropFunctorDrawable::~VkInteropFunctorDrawable() { - if (auto lp = std::get_if<LegacyFunctor>(&mAnyFunctor)) { - if (lp->listener) { - ScopedDrawRequest _drawRequest{}; - lp->listener->onGlFunctorReleased(lp->functor); - } - } -} - void VkInteropFunctorDrawable::syncFunctor(const WebViewSyncData& data) const { ScopedDrawRequest _drawRequest{}; FunctorDrawable::syncFunctor(data); diff --git a/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.h b/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.h index c47ee114263f..e6ea175929c0 100644 --- a/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.h +++ b/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.h @@ -16,11 +16,12 @@ #pragma once -#include "FunctorDrawable.h" - -#include <ui/GraphicBuffer.h> +#include <android/hardware_buffer.h> +#include <utils/NdkUtils.h> #include <utils/RefBase.h> +#include "FunctorDrawable.h" + namespace android { namespace uirenderer { @@ -34,7 +35,7 @@ class VkInteropFunctorDrawable : public FunctorDrawable { public: using FunctorDrawable::FunctorDrawable; - virtual ~VkInteropFunctorDrawable(); + virtual ~VkInteropFunctorDrawable() {} static void vkInvokeFunctor(Functor* functor); @@ -45,7 +46,7 @@ protected: private: // Variables below describe/store temporary offscreen buffer used for Vulkan pipeline. - sp<GraphicBuffer> mFrameBuffer; + UniqueAHardwareBuffer mFrameBuffer; SkImageInfo mFBInfo; }; diff --git a/libs/hwui/renderthread/CacheManager.cpp b/libs/hwui/renderthread/CacheManager.cpp index 1e5877356e8d..85924c5e8939 100644 --- a/libs/hwui/renderthread/CacheManager.cpp +++ b/libs/hwui/renderthread/CacheManager.cpp @@ -61,7 +61,7 @@ CacheManager::CacheManager() SkGraphics::SetFontCacheLimit(mMaxCpuFontCacheBytes); } -void CacheManager::reset(sk_sp<GrContext> context) { +void CacheManager::reset(sk_sp<GrDirectContext> context) { if (context != mGrContext) { destroy(); } @@ -101,7 +101,7 @@ void CacheManager::trimMemory(TrimMemoryMode mode) { return; } - mGrContext->flush(); + mGrContext->flushAndSubmit(); switch (mode) { case TrimMemoryMode::Complete: @@ -122,14 +122,15 @@ void CacheManager::trimMemory(TrimMemoryMode mode) { // We must sync the cpu to make sure deletions of resources still queued up on the GPU actually // happen. - mGrContext->flush(kSyncCpu_GrFlushFlag, 0, nullptr); + mGrContext->flush({}); + mGrContext->submit(true); } void CacheManager::trimStaleResources() { if (!mGrContext) { return; } - mGrContext->flush(); + mGrContext->flushAndSubmit(); mGrContext->purgeResourcesNotUsedInMs(std::chrono::seconds(30)); } diff --git a/libs/hwui/renderthread/CacheManager.h b/libs/hwui/renderthread/CacheManager.h index b009cc4f48f2..0a6b8dc26cc3 100644 --- a/libs/hwui/renderthread/CacheManager.h +++ b/libs/hwui/renderthread/CacheManager.h @@ -18,7 +18,7 @@ #define CACHEMANAGER_H #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration -#include <GrContext.h> +#include <GrDirectContext.h> #endif #include <SkSurface.h> #include <utils/String8.h> @@ -58,13 +58,13 @@ private: explicit CacheManager(); #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration - void reset(sk_sp<GrContext> grContext); + void reset(sk_sp<GrDirectContext> grContext); #endif void destroy(); const size_t mMaxSurfaceArea; #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration - sk_sp<GrContext> mGrContext; + sk_sp<GrDirectContext> mGrContext; #endif const size_t mMaxResourceBytes; diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index 667a7517a24c..eacabfd1dbf9 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -176,7 +176,10 @@ void CanvasContext::setSurface(ANativeWindow* window, bool enableTimeout) { } else { mNativeSurface = nullptr; } + setupPipelineSurface(); +} +void CanvasContext::setupPipelineSurface() { bool hasSurface = mRenderPipeline->setSurface( mNativeSurface ? mNativeSurface->getNativeWindow() : nullptr, mSwapBehavior); @@ -187,7 +190,7 @@ void CanvasContext::setSurface(ANativeWindow* window, bool enableTimeout) { mFrameNumber = -1; - if (window != nullptr && hasSurface) { + if (mNativeSurface != nullptr && hasSurface) { mHaveNewSurface = true; mSwapHistory.clear(); // Enable frame stats after the surface has been bound to the appropriate graphics API. @@ -242,9 +245,9 @@ void CanvasContext::setOpaque(bool opaque) { mOpaque = opaque; } -void CanvasContext::setWideGamut(bool wideGamut) { - ColorMode colorMode = wideGamut ? ColorMode::WideColorGamut : ColorMode::SRGB; - mRenderPipeline->setSurfaceColorProperties(colorMode); +void CanvasContext::setColorMode(ColorMode mode) { + mRenderPipeline->setSurfaceColorProperties(mode); + setupPipelineSurface(); } bool CanvasContext::makeCurrent() { @@ -462,6 +465,7 @@ void CanvasContext::draw() { mCurrentFrameInfo->addFlag(FrameInfoFlags::SkippedFrame); // Notify the callbacks, even if there's nothing to draw so they aren't waiting // indefinitely + waitOnFences(); for (auto& func : mFrameCompleteCallbacks) { std::invoke(func, mFrameNumber); } @@ -484,6 +488,14 @@ void CanvasContext::draw() { waitOnFences(); + if (mNativeSurface) { + // TODO(b/165985262): measure performance impact + if (const auto vsyncId = mCurrentFrameInfo->get(FrameInfoIndex::FrameTimelineVsyncId); + vsyncId != UiFrameInfoBuilder::INVALID_VSYNC_ID) { + native_window_set_frame_timeline_vsync(mNativeSurface->getNativeWindow(), vsyncId); + } + } + bool requireSwap = false; int error = OK; bool didSwap = @@ -617,8 +629,12 @@ void CanvasContext::prepareAndDraw(RenderNode* node) { ATRACE_CALL(); nsecs_t vsync = mRenderThread.timeLord().computeFrameTimeNanos(); + int64_t vsyncId = mRenderThread.timeLord().lastVsyncId(); + int64_t frameDeadline = mRenderThread.timeLord().lastFrameDeadline(); int64_t frameInfo[UI_THREAD_FRAME_INFO_SIZE]; - UiFrameInfoBuilder(frameInfo).addFlag(FrameInfoFlags::RTAnimation).setVsync(vsync, vsync); + UiFrameInfoBuilder(frameInfo) + .addFlag(FrameInfoFlags::RTAnimation) + .setVsync(vsync, vsync, vsyncId, frameDeadline); TreeInfo info(TreeInfo::MODE_RT_ONLY, *this); prepareTree(info, frameInfo, systemTime(SYSTEM_TIME_MONOTONIC), node); diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h index 0f1b8aebf56c..cc4eb3285bbe 100644 --- a/libs/hwui/renderthread/CanvasContext.h +++ b/libs/hwui/renderthread/CanvasContext.h @@ -30,6 +30,7 @@ #include "renderthread/RenderTask.h" #include "renderthread/RenderThread.h" #include "utils/RingBuffer.h" +#include "ColorMode.h" #include <SkBitmap.h> #include <SkRect.h> @@ -105,7 +106,7 @@ public: * If Properties::isSkiaEnabled() is true then this will return the Skia * grContext associated with the current RenderPipeline. */ - GrContext* getGrContext() const { return mRenderThread.getGrContext(); } + GrDirectContext* getGrContext() const { return mRenderThread.getGrContext(); } // Won't take effect until next EGLSurface creation void setSwapBehavior(SwapBehavior swapBehavior); @@ -119,7 +120,7 @@ public: void setLightAlpha(uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha); void setLightGeometry(const Vector3& lightCenter, float lightRadius); void setOpaque(bool opaque); - void setWideGamut(bool wideGamut); + void setColorMode(ColorMode mode); bool makeCurrent(); void prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t syncQueued, RenderNode* target); void draw(); @@ -170,9 +171,9 @@ public: } // Used to queue up work that needs to be completed before this frame completes - ANDROID_API void enqueueFrameWork(std::function<void()>&& func); + void enqueueFrameWork(std::function<void()>&& func); - ANDROID_API int64_t getFrameNumber(); + int64_t getFrameNumber(); void waitOnFences(); @@ -211,6 +212,7 @@ private: bool isSwapChainStuffed(); bool surfaceRequiresRedraw(); void setPresentTime(); + void setupPipelineSurface(); SkRect computeDirtyRect(const Frame& frame, SkRect* dirty); diff --git a/libs/hwui/renderthread/DrawFrameTask.cpp b/libs/hwui/renderthread/DrawFrameTask.cpp index 1e593388d063..c9146b2fc2d1 100644 --- a/libs/hwui/renderthread/DrawFrameTask.cpp +++ b/libs/hwui/renderthread/DrawFrameTask.cpp @@ -128,7 +128,10 @@ void DrawFrameTask::run() { bool DrawFrameTask::syncFrameState(TreeInfo& info) { ATRACE_CALL(); int64_t vsync = mFrameInfo[static_cast<int>(FrameInfoIndex::Vsync)]; - mRenderThread->timeLord().vsyncReceived(vsync); + int64_t intendedVsync = mFrameInfo[static_cast<int>(FrameInfoIndex::IntendedVsync)]; + int64_t vsyncId = mFrameInfo[static_cast<int>(FrameInfoIndex::FrameTimelineVsyncId)]; + int64_t frameDeadline = mFrameInfo[static_cast<int>(FrameInfoIndex::FrameDeadline)]; + mRenderThread->timeLord().vsyncReceived(vsync, intendedVsync, vsyncId, frameDeadline); bool canDraw = mContext->makeCurrent(); mContext->unpinImages(); diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp index 7982ab664c1b..a11678189bad 100644 --- a/libs/hwui/renderthread/EglManager.cpp +++ b/libs/hwui/renderthread/EglManager.cpp @@ -76,6 +76,7 @@ static struct { bool glColorSpace = false; bool scRGB = false; bool displayP3 = false; + bool hdr = false; bool contextPriority = false; bool surfacelessContext = false; bool nativeFenceSync = false; @@ -86,7 +87,8 @@ static struct { EglManager::EglManager() : mEglDisplay(EGL_NO_DISPLAY) , mEglConfig(nullptr) - , mEglConfigWideGamut(nullptr) + , mEglConfigF16(nullptr) + , mEglConfig1010102(nullptr) , mEglContext(EGL_NO_CONTEXT) , mPBufferSurface(EGL_NO_SURFACE) , mCurrentSurface(EGL_NO_SURFACE) @@ -136,15 +138,14 @@ void EglManager::initialize() { LOG_ALWAYS_FATAL_IF(!DeviceInfo::get()->getWideColorSpace()->toXYZD50(&wideColorGamut), "Could not get gamut matrix from wideColorSpace"); bool hasWideColorSpaceExtension = false; - if (memcmp(&wideColorGamut, &SkNamedGamut::kDCIP3, sizeof(wideColorGamut)) == 0) { + if (memcmp(&wideColorGamut, &SkNamedGamut::kDisplayP3, sizeof(wideColorGamut)) == 0) { hasWideColorSpaceExtension = EglExtensions.displayP3; } else if (memcmp(&wideColorGamut, &SkNamedGamut::kSRGB, sizeof(wideColorGamut)) == 0) { hasWideColorSpaceExtension = EglExtensions.scRGB; } else { LOG_ALWAYS_FATAL("Unsupported wide color space."); } - mHasWideColorGamutSupport = EglExtensions.glColorSpace && hasWideColorSpaceExtension && - mEglConfigWideGamut != EGL_NO_CONFIG_KHR; + mHasWideColorGamutSupport = EglExtensions.glColorSpace && hasWideColorSpaceExtension; } EGLConfig EglManager::load8BitsConfig(EGLDisplay display, EglManager::SwapBehavior swapBehavior) { @@ -177,6 +178,35 @@ EGLConfig EglManager::load8BitsConfig(EGLDisplay display, EglManager::SwapBehavi return config; } +EGLConfig EglManager::load1010102Config(EGLDisplay display, SwapBehavior swapBehavior) { + EGLint eglSwapBehavior = + (swapBehavior == SwapBehavior::Preserved) ? EGL_SWAP_BEHAVIOR_PRESERVED_BIT : 0; + // If we reached this point, we have a valid swap behavior + EGLint attribs[] = {EGL_RENDERABLE_TYPE, + EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, + 10, + EGL_GREEN_SIZE, + 10, + EGL_BLUE_SIZE, + 10, + EGL_ALPHA_SIZE, + 2, + EGL_DEPTH_SIZE, + 0, + EGL_STENCIL_SIZE, + STENCIL_BUFFER_SIZE, + EGL_SURFACE_TYPE, + EGL_WINDOW_BIT | eglSwapBehavior, + EGL_NONE}; + EGLConfig config = EGL_NO_CONFIG_KHR; + EGLint numConfigs = 1; + if (!eglChooseConfig(display, attribs, &config, numConfigs, &numConfigs) || numConfigs != 1) { + return EGL_NO_CONFIG_KHR; + } + return config; +} + EGLConfig EglManager::loadFP16Config(EGLDisplay display, SwapBehavior swapBehavior) { EGLint eglSwapBehavior = (swapBehavior == SwapBehavior::Preserved) ? EGL_SWAP_BEHAVIOR_PRESERVED_BIT : 0; @@ -208,12 +238,8 @@ EGLConfig EglManager::loadFP16Config(EGLDisplay display, SwapBehavior swapBehavi return config; } -extern "C" EGLAPI const char* eglQueryStringImplementationANDROID(EGLDisplay dpy, EGLint name); - void EglManager::initExtensions() { auto extensions = StringUtils::split(eglQueryString(mEglDisplay, EGL_EXTENSIONS)); - auto extensionsAndroid = - StringUtils::split(eglQueryStringImplementationANDROID(mEglDisplay, EGL_EXTENSIONS)); // For our purposes we don't care if EGL_BUFFER_AGE is a result of // EGL_EXT_buffer_age or EGL_KHR_partial_update as our usage is covered @@ -230,14 +256,12 @@ void EglManager::initExtensions() { EglExtensions.pixelFormatFloat = extensions.has("EGL_EXT_pixel_format_float"); EglExtensions.scRGB = extensions.has("EGL_EXT_gl_colorspace_scrgb"); EglExtensions.displayP3 = extensions.has("EGL_EXT_gl_colorspace_display_p3_passthrough"); + EglExtensions.hdr = extensions.has("EGL_EXT_gl_colorspace_bt2020_pq"); EglExtensions.contextPriority = extensions.has("EGL_IMG_context_priority"); EglExtensions.surfacelessContext = extensions.has("EGL_KHR_surfaceless_context"); EglExtensions.fenceSync = extensions.has("EGL_KHR_fence_sync"); EglExtensions.waitSync = extensions.has("EGL_KHR_wait_sync"); - - // EGL_ANDROID_native_fence_sync is not exposed to applications, so access - // this through the private Android-specific query instead. - EglExtensions.nativeFenceSync = extensionsAndroid.has("EGL_ANDROID_native_fence_sync"); + EglExtensions.nativeFenceSync = extensions.has("EGL_ANDROID_native_fence_sync"); } bool EglManager::hasEglContext() { @@ -260,18 +284,20 @@ void EglManager::loadConfigs() { LOG_ALWAYS_FATAL("Failed to choose config, error = %s", eglErrorString()); } } - SkColorType wideColorType = DeviceInfo::get()->getWideColorType(); // When we reach this point, we have a valid swap behavior - if (wideColorType == SkColorType::kRGBA_F16_SkColorType && EglExtensions.pixelFormatFloat) { - mEglConfigWideGamut = loadFP16Config(mEglDisplay, mSwapBehavior); - if (mEglConfigWideGamut == EGL_NO_CONFIG_KHR) { + if (EglExtensions.pixelFormatFloat) { + mEglConfigF16 = loadFP16Config(mEglDisplay, mSwapBehavior); + if (mEglConfigF16 == EGL_NO_CONFIG_KHR) { ALOGE("Device claims wide gamut support, cannot find matching config, error = %s", eglErrorString()); EglExtensions.pixelFormatFloat = false; } - } else if (wideColorType == SkColorType::kN32_SkColorType) { - mEglConfigWideGamut = load8BitsConfig(mEglDisplay, mSwapBehavior); + } + mEglConfig1010102 = load1010102Config(mEglDisplay, mSwapBehavior); + if (mEglConfig1010102 == EGL_NO_CONFIG_KHR) { + ALOGW("Failed to initialize 101010-2 format, error = %s", + eglErrorString()); } } @@ -311,8 +337,9 @@ Result<EGLSurface, EGLint> EglManager::createSurface(EGLNativeWindowType window, sk_sp<SkColorSpace> colorSpace) { LOG_ALWAYS_FATAL_IF(!hasEglContext(), "Not initialized"); - bool wideColorGamut = colorMode == ColorMode::WideColorGamut && mHasWideColorGamutSupport && - EglExtensions.noConfigContext; + if (!mHasWideColorGamutSupport || !EglExtensions.noConfigContext) { + colorMode = ColorMode::Default; + } // The color space we want to use depends on whether linear blending is turned // on and whether the app has requested wide color gamut rendering. When wide @@ -338,26 +365,47 @@ Result<EGLSurface, EGLint> EglManager::createSurface(EGLNativeWindowType window, // list is considered empty if the first entry is EGL_NONE EGLint attribs[] = {EGL_NONE, EGL_NONE, EGL_NONE}; + EGLConfig config = mEglConfig; + if (DeviceInfo::get()->getWideColorType() == kRGBA_F16_SkColorType) { + if (mEglConfigF16 == EGL_NO_CONFIG_KHR) { + colorMode = ColorMode::Default; + } else { + config = mEglConfigF16; + } + } if (EglExtensions.glColorSpace) { attribs[0] = EGL_GL_COLORSPACE_KHR; - if (wideColorGamut) { - skcms_Matrix3x3 colorGamut; - LOG_ALWAYS_FATAL_IF(!colorSpace->toXYZD50(&colorGamut), - "Could not get gamut matrix from color space"); - if (memcmp(&colorGamut, &SkNamedGamut::kDCIP3, sizeof(colorGamut)) == 0) { - attribs[1] = EGL_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH_EXT; - } else if (memcmp(&colorGamut, &SkNamedGamut::kSRGB, sizeof(colorGamut)) == 0) { - attribs[1] = EGL_GL_COLORSPACE_SCRGB_EXT; - } else { - LOG_ALWAYS_FATAL("Unreachable: unsupported wide color space."); + switch (colorMode) { + case ColorMode::Default: + attribs[1] = EGL_GL_COLORSPACE_LINEAR_KHR; + break; + case ColorMode::WideColorGamut: { + skcms_Matrix3x3 colorGamut; + LOG_ALWAYS_FATAL_IF(!colorSpace->toXYZD50(&colorGamut), + "Could not get gamut matrix from color space"); + if (memcmp(&colorGamut, &SkNamedGamut::kDisplayP3, sizeof(colorGamut)) == 0) { + attribs[1] = EGL_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH_EXT; + } else if (memcmp(&colorGamut, &SkNamedGamut::kSRGB, sizeof(colorGamut)) == 0) { + attribs[1] = EGL_GL_COLORSPACE_SCRGB_EXT; + } else if (memcmp(&colorGamut, &SkNamedGamut::kRec2020, sizeof(colorGamut)) == 0) { + attribs[1] = EGL_GL_COLORSPACE_BT2020_PQ_EXT; + } else { + LOG_ALWAYS_FATAL("Unreachable: unsupported wide color space."); + } + break; } - } else { - attribs[1] = EGL_GL_COLORSPACE_LINEAR_KHR; + case ColorMode::Hdr: + config = mEglConfigF16; + attribs[1] = EGL_GL_COLORSPACE_BT2020_PQ_EXT; + break; + case ColorMode::Hdr10: + config = mEglConfig1010102; + attribs[1] = EGL_GL_COLORSPACE_BT2020_PQ_EXT; + break; } } - EGLSurface surface = eglCreateWindowSurface( - mEglDisplay, wideColorGamut ? mEglConfigWideGamut : mEglConfig, window, attribs); + EGLSurface surface = eglCreateWindowSurface(mEglDisplay, config, window, attribs); if (surface == EGL_NO_SURFACE) { return Error<EGLint>{eglGetError()}; } diff --git a/libs/hwui/renderthread/EglManager.h b/libs/hwui/renderthread/EglManager.h index a893e245b214..69f3ed014c53 100644 --- a/libs/hwui/renderthread/EglManager.h +++ b/libs/hwui/renderthread/EglManager.h @@ -21,7 +21,6 @@ #include <SkImageInfo.h> #include <SkRect.h> #include <cutils/compiler.h> -#include <ui/GraphicBuffer.h> #include <utils/StrongPointer.h> #include "IRenderPipeline.h" @@ -89,6 +88,7 @@ private: static EGLConfig load8BitsConfig(EGLDisplay display, SwapBehavior swapBehavior); static EGLConfig loadFP16Config(EGLDisplay display, SwapBehavior swapBehavior); + static EGLConfig load1010102Config(EGLDisplay display, SwapBehavior swapBehavior); void initExtensions(); void createPBufferSurface(); @@ -98,7 +98,8 @@ private: EGLDisplay mEglDisplay; EGLConfig mEglConfig; - EGLConfig mEglConfigWideGamut; + EGLConfig mEglConfigF16; + EGLConfig mEglConfig1010102; EGLContext mEglContext; EGLSurface mPBufferSurface; EGLSurface mCurrentSurface; diff --git a/libs/hwui/renderthread/IRenderPipeline.h b/libs/hwui/renderthread/IRenderPipeline.h index c3c22869a42f..aceb5a528fc8 100644 --- a/libs/hwui/renderthread/IRenderPipeline.h +++ b/libs/hwui/renderthread/IRenderPipeline.h @@ -22,11 +22,12 @@ #include "Lighting.h" #include "SwapBehavior.h" #include "hwui/Bitmap.h" +#include "ColorMode.h" #include <SkRect.h> #include <utils/RefBase.h> -class GrContext; +class GrDirectContext; struct ANativeWindow; @@ -42,16 +43,6 @@ namespace renderthread { enum class MakeCurrentResult { AlreadyCurrent, Failed, Succeeded }; -enum class ColorMode { - // SRGB means HWUI will produce buffer in SRGB color space. - SRGB, - // WideColorGamut means HWUI would support rendering scRGB non-linear into - // a signed buffer with enough range to support the wide color gamut of the - // display. - WideColorGamut, - // Hdr -}; - class Frame; class IRenderPipeline { diff --git a/libs/hwui/renderthread/ReliableSurface.cpp b/libs/hwui/renderthread/ReliableSurface.cpp index dcf1fc189588..c29cc11fa7ea 100644 --- a/libs/hwui/renderthread/ReliableSurface.cpp +++ b/libs/hwui/renderthread/ReliableSurface.cpp @@ -149,21 +149,25 @@ ANativeWindowBuffer* ReliableSurface::acquireFallbackBuffer(int error) { return AHardwareBuffer_to_ANativeWindowBuffer(mScratchBuffer.get()); } - AHardwareBuffer_Desc desc; - desc.usage = mUsage; - desc.format = mFormat; - desc.width = 1; - desc.height = 1; - desc.layers = 1; - desc.rfu0 = 0; - desc.rfu1 = 0; - AHardwareBuffer* newBuffer = nullptr; - int err = AHardwareBuffer_allocate(&desc, &newBuffer); - if (err) { + AHardwareBuffer_Desc desc = AHardwareBuffer_Desc{ + .usage = mUsage, + .format = mFormat, + .width = 1, + .height = 1, + .layers = 1, + .rfu0 = 0, + .rfu1 = 0, + }; + + AHardwareBuffer* newBuffer; + int result = AHardwareBuffer_allocate(&desc, &newBuffer); + + if (result != NO_ERROR) { // Allocate failed, that sucks - ALOGW("Failed to allocate scratch buffer, error=%d", err); + ALOGW("Failed to allocate scratch buffer, error=%d", result); return nullptr; } + mScratchBuffer.reset(newBuffer); return AHardwareBuffer_to_ANativeWindowBuffer(newBuffer); } diff --git a/libs/hwui/renderthread/ReliableSurface.h b/libs/hwui/renderthread/ReliableSurface.h index f699eb1fe6b3..41969e776fc8 100644 --- a/libs/hwui/renderthread/ReliableSurface.h +++ b/libs/hwui/renderthread/ReliableSurface.h @@ -21,6 +21,7 @@ #include <apex/window.h> #include <utils/Errors.h> #include <utils/Macros.h> +#include <utils/NdkUtils.h> #include <utils/StrongPointer.h> #include <memory> @@ -67,8 +68,7 @@ private: uint64_t mUsage = AHARDWAREBUFFER_USAGE_GPU_FRAMEBUFFER; AHardwareBuffer_Format mFormat = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; - std::unique_ptr<AHardwareBuffer, void (*)(AHardwareBuffer*)> mScratchBuffer{ - nullptr, AHardwareBuffer_release}; + UniqueAHardwareBuffer mScratchBuffer; ANativeWindowBuffer* mReservedBuffer = nullptr; base::unique_fd mReservedFenceFd; bool mHasDequeuedBuffer = false; diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp index b66a13d1efda..b51f6dcfc66f 100644 --- a/libs/hwui/renderthread/RenderProxy.cpp +++ b/libs/hwui/renderthread/RenderProxy.cpp @@ -77,10 +77,10 @@ void RenderProxy::setName(const char* name) { } void RenderProxy::setSurface(ANativeWindow* window, bool enableTimeout) { - ANativeWindow_acquire(window); + if (window) { ANativeWindow_acquire(window); } mRenderThread.queue().post([this, win = window, enableTimeout]() mutable { mContext->setSurface(win, enableTimeout); - ANativeWindow_release(win); + if (win) { ANativeWindow_release(win); } }); } @@ -109,8 +109,8 @@ void RenderProxy::setOpaque(bool opaque) { mRenderThread.queue().post([=]() { mContext->setOpaque(opaque); }); } -void RenderProxy::setWideGamut(bool wideGamut) { - mRenderThread.queue().post([=]() { mContext->setWideGamut(wideGamut); }); +void RenderProxy::setColorMode(ColorMode mode) { + mRenderThread.queue().post([=]() { mContext->setColorMode(mode); }); } int64_t* RenderProxy::frameInfo() { @@ -128,20 +128,6 @@ void RenderProxy::destroy() { mRenderThread.queue().runSync([=]() { mContext->destroy(); }); } -void RenderProxy::invokeFunctor(Functor* functor, bool waitForCompletion) { - ATRACE_CALL(); - RenderThread& thread = RenderThread::getInstance(); - auto invoke = [&thread, functor]() { CanvasContext::invokeFunctor(thread, functor); }; - if (waitForCompletion) { - // waitForCompletion = true is expected to be fairly rare and only - // happen in destruction. Thus it should be fine to temporarily - // create a Mutex - thread.queue().runSync(std::move(invoke)); - } else { - thread.queue().post(std::move(invoke)); - } -} - void RenderProxy::destroyFunctor(int functor) { ATRACE_CALL(); RenderThread& thread = RenderThread::getInstance(); diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h index 3baeb2f7a476..33dabc9895b1 100644 --- a/libs/hwui/renderthread/RenderProxy.h +++ b/libs/hwui/renderthread/RenderProxy.h @@ -24,6 +24,7 @@ #include "../FrameMetricsObserver.h" #include "../IContextFactory.h" +#include "ColorMode.h" #include "DrawFrameTask.h" #include "SwapBehavior.h" #include "hwui/Bitmap.h" @@ -60,69 +61,67 @@ enum { * references RenderProxy fields. This is safe as RenderProxy cannot * be deleted if it is blocked inside a call. */ -class ANDROID_API RenderProxy { +class RenderProxy { public: - ANDROID_API RenderProxy(bool opaque, RenderNode* rootNode, IContextFactory* contextFactory); - ANDROID_API virtual ~RenderProxy(); + RenderProxy(bool opaque, RenderNode* rootNode, IContextFactory* contextFactory); + virtual ~RenderProxy(); // Won't take effect until next EGLSurface creation - ANDROID_API void setSwapBehavior(SwapBehavior swapBehavior); - ANDROID_API bool loadSystemProperties(); - ANDROID_API void setName(const char* name); - - ANDROID_API void setSurface(ANativeWindow* window, bool enableTimeout = true); - ANDROID_API void allocateBuffers(); - ANDROID_API bool pause(); - ANDROID_API void setStopped(bool stopped); - ANDROID_API void setLightAlpha(uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha); - ANDROID_API void setLightGeometry(const Vector3& lightCenter, float lightRadius); - ANDROID_API void setOpaque(bool opaque); - ANDROID_API void setWideGamut(bool wideGamut); - ANDROID_API int64_t* frameInfo(); - ANDROID_API int syncAndDrawFrame(); - ANDROID_API void destroy(); - - ANDROID_API static void invokeFunctor(Functor* functor, bool waitForCompletion); + void setSwapBehavior(SwapBehavior swapBehavior); + bool loadSystemProperties(); + void setName(const char* name); + + void setSurface(ANativeWindow* window, bool enableTimeout = true); + void allocateBuffers(); + bool pause(); + void setStopped(bool stopped); + void setLightAlpha(uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha); + void setLightGeometry(const Vector3& lightCenter, float lightRadius); + void setOpaque(bool opaque); + void setColorMode(ColorMode mode); + int64_t* frameInfo(); + int syncAndDrawFrame(); + void destroy(); + static void destroyFunctor(int functor); - ANDROID_API DeferredLayerUpdater* createTextureLayer(); - ANDROID_API void buildLayer(RenderNode* node); - ANDROID_API bool copyLayerInto(DeferredLayerUpdater* layer, SkBitmap& bitmap); - ANDROID_API void pushLayerUpdate(DeferredLayerUpdater* layer); - ANDROID_API void cancelLayerUpdate(DeferredLayerUpdater* layer); - ANDROID_API void detachSurfaceTexture(DeferredLayerUpdater* layer); + DeferredLayerUpdater* createTextureLayer(); + void buildLayer(RenderNode* node); + bool copyLayerInto(DeferredLayerUpdater* layer, SkBitmap& bitmap); + void pushLayerUpdate(DeferredLayerUpdater* layer); + void cancelLayerUpdate(DeferredLayerUpdater* layer); + void detachSurfaceTexture(DeferredLayerUpdater* layer); - ANDROID_API void destroyHardwareResources(); - ANDROID_API static void trimMemory(int level); - ANDROID_API static void overrideProperty(const char* name, const char* value); + void destroyHardwareResources(); + static void trimMemory(int level); + static void overrideProperty(const char* name, const char* value); - ANDROID_API void fence(); - ANDROID_API static int maxTextureSize(); - ANDROID_API void stopDrawing(); - ANDROID_API void notifyFramePending(); + void fence(); + static int maxTextureSize(); + void stopDrawing(); + void notifyFramePending(); - ANDROID_API void dumpProfileInfo(int fd, int dumpFlags); + void dumpProfileInfo(int fd, int dumpFlags); // Not exported, only used for testing void resetProfileInfo(); uint32_t frameTimePercentile(int p); - ANDROID_API static void dumpGraphicsMemory(int fd); + static void dumpGraphicsMemory(int fd); - ANDROID_API static void rotateProcessStatsBuffer(); - ANDROID_API static void setProcessStatsBuffer(int fd); - ANDROID_API int getRenderThreadTid(); + static void rotateProcessStatsBuffer(); + static void setProcessStatsBuffer(int fd); + int getRenderThreadTid(); - ANDROID_API void addRenderNode(RenderNode* node, bool placeFront); - ANDROID_API void removeRenderNode(RenderNode* node); - ANDROID_API void drawRenderNode(RenderNode* node); - ANDROID_API void setContentDrawBounds(int left, int top, int right, int bottom); - ANDROID_API void setPictureCapturedCallback( - const std::function<void(sk_sp<SkPicture>&&)>& callback); - ANDROID_API void setFrameCallback(std::function<void(int64_t)>&& callback); - ANDROID_API void setFrameCompleteCallback(std::function<void(int64_t)>&& callback); + void addRenderNode(RenderNode* node, bool placeFront); + void removeRenderNode(RenderNode* node); + void drawRenderNode(RenderNode* node); + void setContentDrawBounds(int left, int top, int right, int bottom); + void setPictureCapturedCallback(const std::function<void(sk_sp<SkPicture>&&)>& callback); + void setFrameCallback(std::function<void(int64_t)>&& callback); + void setFrameCompleteCallback(std::function<void(int64_t)>&& callback); - ANDROID_API void addFrameMetricsObserver(FrameMetricsObserver* observer); - ANDROID_API void removeFrameMetricsObserver(FrameMetricsObserver* observer); - ANDROID_API void setForceDark(bool enable); + void addFrameMetricsObserver(FrameMetricsObserver* observer); + void removeFrameMetricsObserver(FrameMetricsObserver* observer); + void setForceDark(bool enable); /** * Sets a render-ahead depth on the backing renderer. This will increase latency by @@ -139,17 +138,17 @@ public: * * @param renderAhead How far to render ahead, must be in the range [0..2] */ - ANDROID_API void setRenderAheadDepth(int renderAhead); + void setRenderAheadDepth(int renderAhead); - ANDROID_API static int copySurfaceInto(ANativeWindow* window, int left, int top, int right, + static int copySurfaceInto(ANativeWindow* window, int left, int top, int right, int bottom, SkBitmap* bitmap); - ANDROID_API static void prepareToDraw(Bitmap& bitmap); + static void prepareToDraw(Bitmap& bitmap); static int copyHWBitmapInto(Bitmap* hwBitmap, SkBitmap* bitmap); - ANDROID_API static void disableVsync(); + static void disableVsync(); - ANDROID_API static void preload(); + static void preload(); private: RenderThread& mRenderThread; diff --git a/libs/hwui/renderthread/RenderTask.h b/libs/hwui/renderthread/RenderTask.h index c56a3578ad58..3e3a381d65fe 100644 --- a/libs/hwui/renderthread/RenderTask.h +++ b/libs/hwui/renderthread/RenderTask.h @@ -45,12 +45,12 @@ namespace renderthread { * malloc/free churn of small objects? */ -class ANDROID_API RenderTask { +class RenderTask { public: - ANDROID_API RenderTask() : mNext(nullptr), mRunAt(0) {} - ANDROID_API virtual ~RenderTask() {} + RenderTask() : mNext(nullptr), mRunAt(0) {} + virtual ~RenderTask() {} - ANDROID_API virtual void run() = 0; + virtual void run() = 0; RenderTask* mNext; nsecs_t mRunAt; // nano-seconds on the SYSTEM_TIME_MONOTONIC clock diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp index 206b58f62ea7..a101d46f6da0 100644 --- a/libs/hwui/renderthread/RenderThread.cpp +++ b/libs/hwui/renderthread/RenderThread.cpp @@ -51,8 +51,11 @@ static JVMAttachHook gOnStartHook = nullptr; void RenderThread::frameCallback(int64_t frameTimeNanos, void* data) { RenderThread* rt = reinterpret_cast<RenderThread*>(data); + int64_t vsyncId = AChoreographer_getVsyncId(rt->mChoreographer); + int64_t frameDeadline = AChoreographer_getFrameDeadline(rt->mChoreographer); rt->mVsyncRequested = false; - if (rt->timeLord().vsyncReceived(frameTimeNanos) && !rt->mFrameCallbackTaskPending) { + if (rt->timeLord().vsyncReceived(frameTimeNanos, frameTimeNanos, vsyncId, frameDeadline) && + !rt->mFrameCallbackTaskPending) { ATRACE_NAME("queue mFrameCallbackTask"); rt->mFrameCallbackTaskPending = true; nsecs_t runAt = (frameTimeNanos + rt->mDispatchFrameDelay); @@ -131,8 +134,7 @@ RenderThread::RenderThread() , mFrameCallbackTaskPending(false) , mRenderState(nullptr) , mEglManager(nullptr) - , mFunctorManager(WebViewFunctorManager::instance()) - , mVkManager(nullptr) { + , mFunctorManager(WebViewFunctorManager::instance()) { Properties::load(); start("RenderThread"); } @@ -166,7 +168,7 @@ void RenderThread::initThreadLocals() { initializeChoreographer(); mEglManager = new EglManager(); mRenderState = new RenderState(*this); - mVkManager = new VulkanManager(); + mVkManager = VulkanManager::getInstance(); mCacheManager = new CacheManager(); } @@ -190,13 +192,14 @@ void RenderThread::requireGlContext() { auto glesVersion = reinterpret_cast<const char*>(glGetString(GL_VERSION)); auto size = glesVersion ? strlen(glesVersion) : -1; cacheManager().configureContext(&options, glesVersion, size); - sk_sp<GrContext> grContext(GrContext::MakeGL(std::move(glInterface), options)); + sk_sp<GrDirectContext> grContext(GrDirectContext::MakeGL(std::move(glInterface), options)); LOG_ALWAYS_FATAL_IF(!grContext.get()); setGrContext(grContext); } void RenderThread::requireVkContext() { - if (mVkManager->hasVkContext()) { + // the getter creates the context in the event it had been destroyed by destroyRenderingContext + if (vulkanManager().hasVkContext()) { return; } mVkManager->initialize(); @@ -204,7 +207,7 @@ void RenderThread::requireVkContext() { initGrContextOptions(options); auto vkDriverVersion = mVkManager->getDriverVersion(); cacheManager().configureContext(&options, &vkDriverVersion, sizeof(vkDriverVersion)); - sk_sp<GrContext> grContext = mVkManager->createContext(options); + sk_sp<GrDirectContext> grContext = mVkManager->createContext(options); LOG_ALWAYS_FATAL_IF(!grContext.get()); setGrContext(grContext); } @@ -222,11 +225,16 @@ void RenderThread::destroyRenderingContext() { mEglManager->destroy(); } } else { - if (vulkanManager().hasVkContext()) { - setGrContext(nullptr); - vulkanManager().destroy(); - } + setGrContext(nullptr); + mVkManager.clear(); + } +} + +VulkanManager& RenderThread::vulkanManager() { + if (!mVkManager.get()) { + mVkManager = VulkanManager::getInstance(); } + return *mVkManager.get(); } void RenderThread::dumpGraphicsMemory(int fd) { @@ -263,7 +271,7 @@ Readback& RenderThread::readback() { return *mReadback; } -void RenderThread::setGrContext(sk_sp<GrContext> context) { +void RenderThread::setGrContext(sk_sp<GrDirectContext> context) { mCacheManager->reset(context); if (mGrContext) { mRenderState->onContextDestroyed(); diff --git a/libs/hwui/renderthread/RenderThread.h b/libs/hwui/renderthread/RenderThread.h index 8be46a6d16e1..4fbb07168ac0 100644 --- a/libs/hwui/renderthread/RenderThread.h +++ b/libs/hwui/renderthread/RenderThread.h @@ -17,10 +17,10 @@ #ifndef RENDERTHREAD_H_ #define RENDERTHREAD_H_ -#include <GrContext.h> +#include <GrDirectContext.h> #include <SkBitmap.h> -#include <apex/choreographer.h> #include <cutils/compiler.h> +#include <private/android/choreographer.h> #include <thread/ThreadBase.h> #include <utils/Looper.h> #include <utils/Thread.h> @@ -88,7 +88,7 @@ class RenderThread : private ThreadBase { public: // Sets a callback that fires before any RenderThread setup has occurred. - ANDROID_API static void setOnStartHook(JVMAttachHook onStartHook); + static void setOnStartHook(JVMAttachHook onStartHook); static JVMAttachHook getOnStartHook(); WorkQueue& queue() { return ThreadBase::queue(); } @@ -106,11 +106,11 @@ public: ProfileDataContainer& globalProfileData() { return mGlobalProfileData; } Readback& readback(); - GrContext* getGrContext() const { return mGrContext.get(); } - void setGrContext(sk_sp<GrContext> cxt); + GrDirectContext* getGrContext() const { return mGrContext.get(); } + void setGrContext(sk_sp<GrDirectContext> cxt); CacheManager& cacheManager() { return *mCacheManager; } - VulkanManager& vulkanManager() { return *mVkManager; } + VulkanManager& vulkanManager(); sk_sp<Bitmap> allocateHardwareBitmap(SkBitmap& skBitmap); void dumpGraphicsMemory(int fd); @@ -186,9 +186,9 @@ private: ProfileDataContainer mGlobalProfileData; Readback* mReadback = nullptr; - sk_sp<GrContext> mGrContext; + sk_sp<GrDirectContext> mGrContext; CacheManager* mCacheManager; - VulkanManager* mVkManager; + sp<VulkanManager> mVkManager; }; } /* namespace renderthread */ diff --git a/libs/hwui/renderthread/TimeLord.cpp b/libs/hwui/renderthread/TimeLord.cpp index 784068f1d877..abb633028363 100644 --- a/libs/hwui/renderthread/TimeLord.cpp +++ b/libs/hwui/renderthread/TimeLord.cpp @@ -14,14 +14,26 @@ * limitations under the License. */ #include "TimeLord.h" +#include <limits> namespace android { namespace uirenderer { namespace renderthread { -TimeLord::TimeLord() : mFrameIntervalNanos(milliseconds_to_nanoseconds(16)), mFrameTimeNanos(0) {} +TimeLord::TimeLord() : mFrameIntervalNanos(milliseconds_to_nanoseconds(16)), + mFrameTimeNanos(0), + mFrameIntendedTimeNanos(0), + mFrameVsyncId(-1), + mFrameDeadline(std::numeric_limits<int64_t>::max()){} + +bool TimeLord::vsyncReceived(nsecs_t vsync, nsecs_t intendedVsync, int64_t vsyncId, + int64_t frameDeadline) { + if (intendedVsync > mFrameIntendedTimeNanos) { + mFrameIntendedTimeNanos = intendedVsync; + mFrameVsyncId = vsyncId; + mFrameDeadline = frameDeadline; + } -bool TimeLord::vsyncReceived(nsecs_t vsync) { if (vsync > mFrameTimeNanos) { mFrameTimeNanos = vsync; return true; @@ -36,6 +48,8 @@ nsecs_t TimeLord::computeFrameTimeNanos() { if (jitterNanos >= mFrameIntervalNanos) { nsecs_t lastFrameOffset = jitterNanos % mFrameIntervalNanos; mFrameTimeNanos = now - lastFrameOffset; + // mFrameVsyncId is not adjusted here as we still want to send + // the vsync id that started this frame to the Surface Composer } return mFrameTimeNanos; } diff --git a/libs/hwui/renderthread/TimeLord.h b/libs/hwui/renderthread/TimeLord.h index 68a0f7f971b9..fa05c030fa0f 100644 --- a/libs/hwui/renderthread/TimeLord.h +++ b/libs/hwui/renderthread/TimeLord.h @@ -32,9 +32,12 @@ public: nsecs_t frameIntervalNanos() const { return mFrameIntervalNanos; } // returns true if the vsync is newer, false if it was rejected for staleness - bool vsyncReceived(nsecs_t vsync); + bool vsyncReceived(nsecs_t vsync, nsecs_t indendedVsync, int64_t vsyncId, + int64_t frameDeadline); nsecs_t latestVsync() { return mFrameTimeNanos; } nsecs_t computeFrameTimeNanos(); + int64_t lastVsyncId() const { return mFrameVsyncId; } + int64_t lastFrameDeadline() const { return mFrameDeadline; } private: friend class RenderThread; @@ -44,6 +47,9 @@ private: nsecs_t mFrameIntervalNanos; nsecs_t mFrameTimeNanos; + nsecs_t mFrameIntendedTimeNanos; + int64_t mFrameVsyncId; + int64_t mFrameDeadline; }; } /* namespace renderthread */ diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index ba70afc8b8d2..1333b92037c3 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -20,7 +20,7 @@ #include <EGL/eglext.h> #include <GrBackendSemaphore.h> #include <GrBackendSurface.h> -#include <GrContext.h> +#include <GrDirectContext.h> #include <GrTypes.h> #include <android/sync.h> #include <ui/FatVector.h> @@ -57,12 +57,22 @@ static void free_features_extensions_structs(const VkPhysicalDeviceFeatures2& fe #define GET_INST_PROC(F) m##F = (PFN_vk##F)vkGetInstanceProcAddr(mInstance, "vk" #F) #define GET_DEV_PROC(F) m##F = (PFN_vk##F)vkGetDeviceProcAddr(mDevice, "vk" #F) -void VulkanManager::destroy() { - if (VK_NULL_HANDLE != mCommandPool) { - mDestroyCommandPool(mDevice, mCommandPool, nullptr); - mCommandPool = VK_NULL_HANDLE; +sp<VulkanManager> VulkanManager::getInstance() { + // cache a weakptr to the context to enable a second thread to share the same vulkan state + static wp<VulkanManager> sWeakInstance = nullptr; + static std::mutex sLock; + + std::lock_guard _lock{sLock}; + sp<VulkanManager> vulkanManager = sWeakInstance.promote(); + if (!vulkanManager.get()) { + vulkanManager = new VulkanManager(); + sWeakInstance = vulkanManager; } + return vulkanManager; +} + +VulkanManager::~VulkanManager() { if (mDevice != VK_NULL_HANDLE) { mDeviceWaitIdle(mDevice); mDestroyDevice(mDevice, nullptr); @@ -73,7 +83,7 @@ void VulkanManager::destroy() { } mGraphicsQueue = VK_NULL_HANDLE; - mPresentQueue = VK_NULL_HANDLE; + mAHBUploadQueue = VK_NULL_HANDLE; mDevice = VK_NULL_HANDLE; mPhysicalDevice = VK_NULL_HANDLE; mInstance = VK_NULL_HANDLE; @@ -175,15 +185,12 @@ void VulkanManager::setupDevice(GrVkExtensions& grExtensions, VkPhysicalDeviceFe for (uint32_t i = 0; i < queueCount; i++) { if (queueProps[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { mGraphicsQueueIndex = i; + LOG_ALWAYS_FATAL_IF(queueProps[i].queueCount < 2); break; } } 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, @@ -277,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 - 1, // 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 @@ -347,29 +344,15 @@ void VulkanManager::initialize() { this->setupDevice(mExtensions, mPhysicalDeviceFeatures2); mGetDeviceQueue(mDevice, mGraphicsQueueIndex, 0, &mGraphicsQueue); - - // create the command pool for the command buffers - if (VK_NULL_HANDLE == mCommandPool) { - VkCommandPoolCreateInfo commandPoolInfo; - memset(&commandPoolInfo, 0, sizeof(VkCommandPoolCreateInfo)); - commandPoolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; - // this needs to be on the render queue - commandPoolInfo.queueFamilyIndex = mGraphicsQueueIndex; - commandPoolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; - SkDEBUGCODE(VkResult res =) - mCreateCommandPool(mDevice, &commandPoolInfo, nullptr, &mCommandPool); - SkASSERT(VK_SUCCESS == res); - } - LOG_ALWAYS_FATAL_IF(mCommandPool == VK_NULL_HANDLE); - - mGetDeviceQueue(mDevice, mPresentQueueIndex, 0, &mPresentQueue); + mGetDeviceQueue(mDevice, mGraphicsQueueIndex, 1, &mAHBUploadQueue); if (Properties::enablePartialUpdates && Properties::useBufferAge) { mSwapBehavior = SwapBehavior::BufferAge; } } -sk_sp<GrContext> VulkanManager::createContext(const GrContextOptions& options) { +sk_sp<GrDirectContext> VulkanManager::createContext(const GrContextOptions& options, + ContextType contextType) { auto getProc = [](const char* proc_name, VkInstance instance, VkDevice device) { if (device != VK_NULL_HANDLE) { return vkGetDeviceProcAddr(device, proc_name); @@ -381,14 +364,15 @@ sk_sp<GrContext> VulkanManager::createContext(const GrContextOptions& options) { backendContext.fInstance = mInstance; backendContext.fPhysicalDevice = mPhysicalDevice; backendContext.fDevice = mDevice; - backendContext.fQueue = mGraphicsQueue; + backendContext.fQueue = (contextType == ContextType::kRenderThread) ? mGraphicsQueue + : mAHBUploadQueue; backendContext.fGraphicsQueueIndex = mGraphicsQueueIndex; backendContext.fMaxAPIVersion = mAPIVersion; backendContext.fVkExtensions = &mExtensions; backendContext.fDeviceFeatures2 = &mPhysicalDeviceFeatures2; backendContext.fGetProc = std::move(getProc); - return GrContext::MakeVulkan(backendContext, options); + return GrDirectContext::MakeVulkan(backendContext, options); } VkFunctorInitParams VulkanManager::getVkFunctorInitParams() const { @@ -459,7 +443,7 @@ Frame VulkanManager::dequeueNextBuffer(VulkanSurface* surface) { // The following flush blocks the GPU immediately instead of waiting for other // drawing ops. It seems dequeue_fence is not respected otherwise. // TODO: remove the flush after finding why backendSemaphore is not working. - bufferInfo->skSurface->flush(); + bufferInfo->skSurface->flushAndSubmit(); } } } @@ -525,9 +509,16 @@ void VulkanManager::swapBuffers(VulkanSurface* surface, const SkRect& dirtyRect) int fenceFd = -1; DestroySemaphoreInfo* destroyInfo = new DestroySemaphoreInfo(mDestroySemaphore, mDevice, semaphore); + GrFlushInfo flushInfo; + flushInfo.fNumSemaphores = 1; + flushInfo.fSignalSemaphores = &backendSemaphore; + flushInfo.fFinishedProc = destroy_semaphore; + flushInfo.fFinishedContext = destroyInfo; GrSemaphoresSubmitted submitted = bufferInfo->skSurface->flush( - SkSurface::BackendSurfaceAccess::kPresent, kNone_GrFlushFlags, 1, &backendSemaphore, - destroy_semaphore, destroyInfo); + SkSurface::BackendSurfaceAccess::kPresent, flushInfo); + GrDirectContext* context = GrAsDirectContext(bufferInfo->skSurface->recordingContext()); + ALOGE_IF(!context, "Surface is not backed by gpu"); + context->submit(); if (submitted == GrSemaphoresSubmitted::kYes) { VkSemaphoreGetFdInfoKHR getFdInfo; getFdInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR; @@ -548,17 +539,19 @@ 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); delete surface; } -VulkanSurface* VulkanManager::createSurface(ANativeWindow* window, ColorMode colorMode, +VulkanSurface* VulkanManager::createSurface(ANativeWindow* window, + ColorMode colorMode, sk_sp<SkColorSpace> surfaceColorSpace, - SkColorType surfaceColorType, GrContext* grContext, + SkColorType surfaceColorType, + GrDirectContext* grContext, uint32_t extraBuffers) { LOG_ALWAYS_FATAL_IF(!hasVkContext(), "Not initialized"); if (!window) { @@ -569,7 +562,7 @@ VulkanSurface* VulkanManager::createSurface(ANativeWindow* window, ColorMode col *this, extraBuffers); } -status_t VulkanManager::fenceWait(int fence, GrContext* grContext) { +status_t VulkanManager::fenceWait(int fence, GrDirectContext* grContext) { if (!hasVkContext()) { ALOGE("VulkanManager::fenceWait: VkDevice not initialized"); return INVALID_OPERATION; @@ -612,12 +605,12 @@ status_t VulkanManager::fenceWait(int fence, GrContext* grContext) { // Skia takes ownership of the semaphore and will delete it once the wait has finished. grContext->wait(1, &beSemaphore); - grContext->flush(); + grContext->flushAndSubmit(); return OK; } -status_t VulkanManager::createReleaseFence(int* nativeFence, GrContext* grContext) { +status_t VulkanManager::createReleaseFence(int* nativeFence, GrDirectContext* grContext) { *nativeFence = -1; if (!hasVkContext()) { ALOGE("VulkanManager::createReleaseFence: VkDevice not initialized"); @@ -648,8 +641,13 @@ status_t VulkanManager::createReleaseFence(int* nativeFence, GrContext* grContex // Even if Skia fails to submit the semaphore, it will still call the destroy_semaphore callback // which will remove its ref to the semaphore. The VulkanManager must still release its ref, // when it is done with the semaphore. - GrSemaphoresSubmitted submitted = grContext->flush(kNone_GrFlushFlags, 1, &backendSemaphore, - destroy_semaphore, destroyInfo); + GrFlushInfo flushInfo; + flushInfo.fNumSemaphores = 1; + flushInfo.fSignalSemaphores = &backendSemaphore; + flushInfo.fFinishedProc = destroy_semaphore; + flushInfo.fFinishedContext = destroyInfo; + GrSemaphoresSubmitted submitted = grContext->flush(flushInfo); + grContext->submit(); if (submitted == GrSemaphoresSubmitted::kNo) { ALOGE("VulkanManager::createReleaseFence: Failed to submit semaphore"); diff --git a/libs/hwui/renderthread/VulkanManager.h b/libs/hwui/renderthread/VulkanManager.h index 8b19f13fdfb9..7a77466303cd 100644 --- a/libs/hwui/renderthread/VulkanManager.h +++ b/libs/hwui/renderthread/VulkanManager.h @@ -43,10 +43,9 @@ class RenderThread; // This class contains the shared global Vulkan objects, such as VkInstance, VkDevice and VkQueue, // which are re-used by CanvasContext. This class is created once and should be used by all vulkan // windowing contexts. The VulkanManager must be initialized before use. -class VulkanManager { +class VulkanManager final : public RefBase { public: - explicit VulkanManager() {} - ~VulkanManager() { destroy(); } + static sp<VulkanManager> getInstance(); // Sets up the vulkan context that is shared amonst all clients of the VulkanManager. This must // be call once before use of the VulkanManager. Multiple calls after the first will simiply @@ -57,36 +56,47 @@ public: bool hasVkContext() { return mDevice != VK_NULL_HANDLE; } // Create and destroy functions for wrapping an ANativeWindow in a VulkanSurface - VulkanSurface* createSurface(ANativeWindow* window, ColorMode colorMode, + VulkanSurface* createSurface(ANativeWindow* window, + ColorMode colorMode, sk_sp<SkColorSpace> surfaceColorSpace, - SkColorType surfaceColorType, GrContext* grContext, + SkColorType surfaceColorType, + GrDirectContext* grContext, uint32_t extraBuffers); void destroySurface(VulkanSurface* surface); Frame dequeueNextBuffer(VulkanSurface* surface); void swapBuffers(VulkanSurface* surface, const SkRect& dirtyRect); - // Cleans up all the global state in the VulkanManger. - void destroy(); - // Inserts a wait on fence command into the Vulkan command buffer. - status_t fenceWait(int fence, GrContext* grContext); + status_t fenceWait(int fence, GrDirectContext* grContext); // Creates a fence that is signaled when all the pending Vulkan commands are finished on the // GPU. - status_t createReleaseFence(int* nativeFence, GrContext* grContext); + status_t createReleaseFence(int* nativeFence, GrDirectContext* grContext); // Returned pointers are owned by VulkanManager. // An instance of VkFunctorInitParams returned from getVkFunctorInitParams refers to // the internal state of VulkanManager: VulkanManager must be alive to use the returned value. VkFunctorInitParams getVkFunctorInitParams() const; - sk_sp<GrContext> createContext(const GrContextOptions& options); + + enum class ContextType { + kRenderThread, + kUploadThread + }; + + // returns a Skia graphic context used to draw content on the specified thread + sk_sp<GrDirectContext> createContext(const GrContextOptions& options, + ContextType contextType = ContextType::kRenderThread); uint32_t getDriverVersion() const { return mDriverVersion; } private: friend class VulkanSurface; + + explicit VulkanManager() {} + ~VulkanManager(); + // Sets up the VkInstance and VkDevice objects. Also fills out the passed in // VkPhysicalDeviceFeatures struct. void setupDevice(GrVkExtensions&, VkPhysicalDeviceFeatures2&); @@ -152,9 +162,7 @@ private: uint32_t mGraphicsQueueIndex; VkQueue mGraphicsQueue = VK_NULL_HANDLE; - uint32_t mPresentQueueIndex; - VkQueue mPresentQueue = VK_NULL_HANDLE; - VkCommandPool mCommandPool = VK_NULL_HANDLE; + VkQueue mAHBUploadQueue = VK_NULL_HANDLE; // Variables saved to populate VkFunctorInitParams. static const uint32_t mAPIVersion = VK_MAKE_VERSION(1, 1, 0); diff --git a/libs/hwui/renderthread/VulkanSurface.cpp b/libs/hwui/renderthread/VulkanSurface.cpp index a7ea21d8c4de..acf4931d6144 100644 --- a/libs/hwui/renderthread/VulkanSurface.cpp +++ b/libs/hwui/renderthread/VulkanSurface.cpp @@ -16,6 +16,7 @@ #include "VulkanSurface.h" +#include <GrDirectContext.h> #include <SkSurface.h> #include <algorithm> @@ -117,7 +118,7 @@ static bool ConnectAndSetWindowDefaults(ANativeWindow* window) { VulkanSurface* VulkanSurface::Create(ANativeWindow* window, ColorMode colorMode, SkColorType colorType, sk_sp<SkColorSpace> colorSpace, - GrContext* grContext, const VulkanManager& vkManager, + GrDirectContext* grContext, const VulkanManager& vkManager, uint32_t extraBuffers) { // Connect and set native window to default configurations. if (!ConnectAndSetWindowDefaults(window)) { @@ -200,16 +201,16 @@ bool VulkanSurface::InitializeWindowInfoStruct(ANativeWindow* window, ColorMode "Could not get gamut matrix from color space"); if (memcmp(&surfaceGamut, &SkNamedGamut::kSRGB, sizeof(surfaceGamut)) == 0) { outWindowInfo->dataspace = HAL_DATASPACE_V0_SCRGB; - } else if (memcmp(&surfaceGamut, &SkNamedGamut::kDCIP3, sizeof(surfaceGamut)) == 0) { + } else if (memcmp(&surfaceGamut, &SkNamedGamut::kDisplayP3, sizeof(surfaceGamut)) == 0) { outWindowInfo->dataspace = HAL_DATASPACE_DISPLAY_P3; } else { LOG_ALWAYS_FATAL("Unreachable: unsupported wide color space."); } } - outWindowInfo->pixelFormat = ColorTypeToPixelFormat(colorType); + outWindowInfo->bufferFormat = ColorTypeToBufferFormat(colorType); VkFormat vkPixelFormat = VK_FORMAT_R8G8B8A8_UNORM; - if (outWindowInfo->pixelFormat == PIXEL_FORMAT_RGBA_FP16) { + if (outWindowInfo->bufferFormat == AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT) { vkPixelFormat = VK_FORMAT_R16G16B16A16_SFLOAT; } @@ -263,10 +264,10 @@ bool VulkanSurface::InitializeWindowInfoStruct(ANativeWindow* window, ColorMode bool VulkanSurface::UpdateWindow(ANativeWindow* window, const WindowInfo& windowInfo) { ATRACE_CALL(); - int err = native_window_set_buffers_format(window, windowInfo.pixelFormat); + int err = native_window_set_buffers_format(window, windowInfo.bufferFormat); if (err != 0) { ALOGE("VulkanSurface::UpdateWindow() native_window_set_buffers_format(%d) failed: %s (%d)", - windowInfo.pixelFormat, strerror(-err), err); + windowInfo.bufferFormat, strerror(-err), err); return false; } @@ -310,7 +311,7 @@ bool VulkanSurface::UpdateWindow(ANativeWindow* window, const WindowInfo& window } VulkanSurface::VulkanSurface(ANativeWindow* window, const WindowInfo& windowInfo, - GrContext* grContext) + GrDirectContext* grContext) : mNativeWindow(window), mWindowInfo(windowInfo), mGrContext(grContext) {} VulkanSurface::~VulkanSurface() { diff --git a/libs/hwui/renderthread/VulkanSurface.h b/libs/hwui/renderthread/VulkanSurface.h index bd2362612a13..409921bdfdd7 100644 --- a/libs/hwui/renderthread/VulkanSurface.h +++ b/libs/hwui/renderthread/VulkanSurface.h @@ -17,8 +17,6 @@ #include <system/graphics.h> #include <system/window.h> -#include <ui/BufferQueueDefs.h> -#include <ui/PixelFormat.h> #include <vulkan/vulkan.h> #include <SkRefCnt.h> @@ -37,7 +35,7 @@ class VulkanManager; class VulkanSurface { public: static VulkanSurface* Create(ANativeWindow* window, ColorMode colorMode, SkColorType colorType, - sk_sp<SkColorSpace> colorSpace, GrContext* grContext, + sk_sp<SkColorSpace> colorSpace, GrDirectContext* grContext, const VulkanManager& vkManager, uint32_t extraBuffers); ~VulkanSurface(); @@ -91,7 +89,7 @@ private: struct WindowInfo { SkISize size; - PixelFormat pixelFormat; + uint32_t bufferFormat; android_dataspace dataspace; int transform; size_t bufferCount; @@ -103,7 +101,7 @@ private: SkMatrix preTransform; }; - VulkanSurface(ANativeWindow* window, const WindowInfo& windowInfo, GrContext* grContext); + VulkanSurface(ANativeWindow* window, const WindowInfo& windowInfo, GrDirectContext* grContext); static bool InitializeWindowInfoStruct(ANativeWindow* window, ColorMode colorMode, SkColorType colorType, sk_sp<SkColorSpace> colorSpace, const VulkanManager& vkManager, uint32_t extraBuffers, @@ -111,12 +109,17 @@ private: static bool UpdateWindow(ANativeWindow* window, const WindowInfo& windowInfo); void releaseBuffers(); + // TODO: This number comes from ui/BufferQueueDefs. We're not pulling the + // header in so that we don't need to depend on libui, but we should share + // this constant somewhere. But right now it's okay to keep here because we + // can't safely change the slot count anyways. + static constexpr size_t kNumBufferSlots = 64; // TODO: Just use a vector? - NativeBufferInfo mNativeBuffers[android::BufferQueueDefs::NUM_BUFFER_SLOTS]; + NativeBufferInfo mNativeBuffers[kNumBufferSlots]; sp<ANativeWindow> mNativeWindow; WindowInfo mWindowInfo; - GrContext* mGrContext; + GrDirectContext* mGrContext; uint32_t mPresentCount = 0; NativeBufferInfo* mCurrentBufferInfo = nullptr; diff --git a/libs/hwui/service/GraphicsStatsService.h b/libs/hwui/service/GraphicsStatsService.h index 59e21d039c9d..4063f749f808 100644 --- a/libs/hwui/service/GraphicsStatsService.h +++ b/libs/hwui/service/GraphicsStatsService.h @@ -44,18 +44,16 @@ public: ProtobufStatsd, }; - ANDROID_API static void saveBuffer(const std::string& path, const std::string& package, - int64_t versionCode, int64_t startTime, int64_t endTime, - const ProfileData* data); - - ANDROID_API static Dump* createDump(int outFd, DumpType type); - ANDROID_API static void addToDump(Dump* dump, const std::string& path, - const std::string& package, int64_t versionCode, - int64_t startTime, int64_t endTime, const ProfileData* data); - ANDROID_API static void addToDump(Dump* dump, const std::string& path); - ANDROID_API static void finishDump(Dump* dump); - ANDROID_API static void finishDumpInMemory(Dump* dump, AStatsEventList* data, - bool lastFullDay); + static void saveBuffer(const std::string& path, const std::string& package, int64_t versionCode, + int64_t startTime, int64_t endTime, const ProfileData* data); + + static Dump* createDump(int outFd, DumpType type); + static void addToDump(Dump* dump, const std::string& path, const std::string& package, + int64_t versionCode, int64_t startTime, int64_t endTime, + const ProfileData* data); + static void addToDump(Dump* dump, const std::string& path); + static void finishDump(Dump* dump); + static void finishDumpInMemory(Dump* dump, AStatsEventList* data, bool lastFullDay); // Visible for testing static bool parseFromFile(const std::string& path, protos::GraphicsStatsProto* output); diff --git a/libs/hwui/shader/BlurShader.cpp b/libs/hwui/shader/BlurShader.cpp new file mode 100644 index 000000000000..2abd8714204b --- /dev/null +++ b/libs/hwui/shader/BlurShader.cpp @@ -0,0 +1,41 @@ +/* + * 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. + */ + +#include "BlurShader.h" +#include "SkImageFilters.h" +#include "SkRefCnt.h" +#include "utils/Blur.h" + +namespace android::uirenderer { +BlurShader::BlurShader(float radiusX, float radiusY, Shader* inputShader, SkTileMode edgeTreatment, + const SkMatrix* matrix) + : Shader(matrix) + , skImageFilter( + SkImageFilters::Blur( + Blur::convertRadiusToSigma(radiusX), + Blur::convertRadiusToSigma(radiusY), + edgeTreatment, + inputShader ? inputShader->asSkImageFilter() : nullptr, + nullptr) + ) { } + +sk_sp<SkImageFilter> BlurShader::makeSkImageFilter() { + return skImageFilter; +} + +BlurShader::~BlurShader() {} + +} // namespace android::uirenderer
\ No newline at end of file diff --git a/libs/hwui/shader/BlurShader.h b/libs/hwui/shader/BlurShader.h new file mode 100644 index 000000000000..60a15898893e --- /dev/null +++ b/libs/hwui/shader/BlurShader.h @@ -0,0 +1,46 @@ +/* + * 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. + */ + +#pragma once +#include "Shader.h" + +namespace android::uirenderer { + +/** + * Shader implementation that blurs another Shader instance or the source bitmap + */ +class BlurShader : public Shader { +public: + /** + * Creates a BlurShader instance with the provided radius values to blur along the x and y + * axis accordingly. + * + * This will blur the contents of the provided input shader if it is non-null, otherwise + * the source bitmap will be blurred instead. + * + * The edge treatment parameter determines how content near the edges of the source is to + * participate in the blur + */ + BlurShader(float radiusX, float radiusY, Shader* inputShader, SkTileMode edgeTreatment, + const SkMatrix* matrix); + ~BlurShader() override; +protected: + sk_sp<SkImageFilter> makeSkImageFilter() override; +private: + sk_sp<SkImageFilter> skImageFilter; +}; + +} // namespace android::uirenderer
\ No newline at end of file diff --git a/libs/hwui/tests/common/TestUtils.h b/libs/hwui/tests/common/TestUtils.h index 91a808df3657..36c5a8c1b3de 100644 --- a/libs/hwui/tests/common/TestUtils.h +++ b/libs/hwui/tests/common/TestUtils.h @@ -287,18 +287,6 @@ public: static std::unique_ptr<uint16_t[]> asciiToUtf16(const char* str); - class MockFunctor : public Functor { - public: - virtual status_t operator()(int what, void* data) { - mLastMode = what; - return DrawGlInfo::kStatusDone; - } - int getLastMode() const { return mLastMode; } - - private: - int mLastMode = -1; - }; - static SkColor getColor(const sk_sp<SkSurface>& surface, int x, int y); static SkRect getClipBounds(const SkCanvas* canvas); @@ -311,30 +299,32 @@ public: int glesDraw = 0; }; - static void expectOnRenderThread() { EXPECT_EQ(gettid(), TestUtils::getRenderThreadTid()); } + static void expectOnRenderThread(const std::string_view& function = "unknown") { + EXPECT_EQ(gettid(), TestUtils::getRenderThreadTid()) << "Called on wrong thread: " << function; + } static WebViewFunctorCallbacks createMockFunctor(RenderMode mode) { auto callbacks = WebViewFunctorCallbacks{ .onSync = [](int functor, void* client_data, const WebViewSyncData& data) { - expectOnRenderThread(); + expectOnRenderThread("onSync"); sMockFunctorCounts[functor].sync++; }, .onContextDestroyed = [](int functor, void* client_data) { - expectOnRenderThread(); + expectOnRenderThread("onContextDestroyed"); sMockFunctorCounts[functor].contextDestroyed++; }, .onDestroyed = [](int functor, void* client_data) { - expectOnRenderThread(); + expectOnRenderThread("onDestroyed"); sMockFunctorCounts[functor].destroyed++; }, }; switch (mode) { case RenderMode::OpenGL_ES: callbacks.gles.draw = [](int functor, void* client_data, const DrawGlInfo& params) { - expectOnRenderThread(); + expectOnRenderThread("draw"); sMockFunctorCounts[functor].glesDraw++; }; break; diff --git a/libs/hwui/tests/common/scenes/MagnifierAnimation.cpp b/libs/hwui/tests/common/scenes/MagnifierAnimation.cpp index f4fce277454d..edadf78db051 100644 --- a/libs/hwui/tests/common/scenes/MagnifierAnimation.cpp +++ b/libs/hwui/tests/common/scenes/MagnifierAnimation.cpp @@ -56,9 +56,9 @@ public: (float)magnifier->height(), 0, 0, (float)props.getWidth(), (float)props.getHeight(), nullptr); }); - canvas.insertReorderBarrier(true); + canvas.enableZ(true); canvas.drawRenderNode(zoomImageView.get()); - canvas.insertReorderBarrier(false); + canvas.enableZ(false); } void doFrame(int frameNr) override { diff --git a/libs/hwui/tests/common/scenes/RecentsAnimation.cpp b/libs/hwui/tests/common/scenes/RecentsAnimation.cpp index 3480a0f18407..1c2507867f6e 100644 --- a/libs/hwui/tests/common/scenes/RecentsAnimation.cpp +++ b/libs/hwui/tests/common/scenes/RecentsAnimation.cpp @@ -36,7 +36,7 @@ public: int cardsize = std::min(width, height) - dp(64); renderer.drawColor(Color::White, SkBlendMode::kSrcOver); - renderer.insertReorderBarrier(true); + renderer.enableZ(true); int x = dp(32); for (int i = 0; i < 4; i++) { @@ -52,7 +52,7 @@ public: mCards.push_back(card); } - renderer.insertReorderBarrier(false); + renderer.enableZ(false); } void doFrame(int frameNr) override { diff --git a/libs/hwui/tests/common/scenes/RectGridAnimation.cpp b/libs/hwui/tests/common/scenes/RectGridAnimation.cpp index 80b5cc191089..f37bcbc3ee1b 100644 --- a/libs/hwui/tests/common/scenes/RectGridAnimation.cpp +++ b/libs/hwui/tests/common/scenes/RectGridAnimation.cpp @@ -29,7 +29,7 @@ public: sp<RenderNode> card; void createContent(int width, int height, Canvas& canvas) override { canvas.drawColor(0xFFFFFFFF, SkBlendMode::kSrcOver); - canvas.insertReorderBarrier(true); + canvas.enableZ(true); card = TestUtils::createNode(50, 50, 250, 250, [](RenderProperties& props, Canvas& canvas) { canvas.drawColor(0xFFFF00FF, SkBlendMode::kSrcOver); @@ -47,7 +47,7 @@ public: }); canvas.drawRenderNode(card.get()); - canvas.insertReorderBarrier(false); + canvas.enableZ(false); } void doFrame(int frameNr) override { int curFrame = frameNr % 150; diff --git a/libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp b/libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp index 314e922e9f38..163745b04ed2 100644 --- a/libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp +++ b/libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp @@ -27,7 +27,7 @@ public: std::vector<sp<RenderNode> > cards; void createContent(int width, int height, Canvas& canvas) override { canvas.drawColor(0xFFFFFFFF, SkBlendMode::kSrcOver); - canvas.insertReorderBarrier(true); + canvas.enableZ(true); int ci = 0; for (int x = 0; x < width; x += mSpacing) { @@ -45,7 +45,7 @@ public: } } - canvas.insertReorderBarrier(false); + canvas.enableZ(false); } void doFrame(int frameNr) override { int curFrame = frameNr % 50; diff --git a/libs/hwui/tests/common/scenes/ShadowGrid2Animation.cpp b/libs/hwui/tests/common/scenes/ShadowGrid2Animation.cpp index bdc991ba1890..c13e80e8c204 100644 --- a/libs/hwui/tests/common/scenes/ShadowGrid2Animation.cpp +++ b/libs/hwui/tests/common/scenes/ShadowGrid2Animation.cpp @@ -29,7 +29,7 @@ public: std::vector<sp<RenderNode> > cards; void createContent(int width, int height, Canvas& canvas) override { canvas.drawColor(0xFFFFFFFF, SkBlendMode::kSrcOver); - canvas.insertReorderBarrier(true); + canvas.enableZ(true); for (int x = dp(8); x < (width - dp(58)); x += dp(58)) { for (int y = dp(8); y < (height - dp(58)); y += dp(58)) { @@ -39,7 +39,7 @@ public: } } - canvas.insertReorderBarrier(false); + canvas.enableZ(false); } void doFrame(int frameNr) override { int curFrame = frameNr % 150; diff --git a/libs/hwui/tests/common/scenes/ShadowGridAnimation.cpp b/libs/hwui/tests/common/scenes/ShadowGridAnimation.cpp index a12fd4d69280..772b98e32220 100644 --- a/libs/hwui/tests/common/scenes/ShadowGridAnimation.cpp +++ b/libs/hwui/tests/common/scenes/ShadowGridAnimation.cpp @@ -29,7 +29,7 @@ public: std::vector<sp<RenderNode> > cards; void createContent(int width, int height, Canvas& canvas) override { canvas.drawColor(0xFFFFFFFF, SkBlendMode::kSrcOver); - canvas.insertReorderBarrier(true); + canvas.enableZ(true); for (int x = dp(16); x < (width - dp(116)); x += dp(116)) { for (int y = dp(16); y < (height - dp(116)); y += dp(116)) { @@ -39,7 +39,7 @@ public: } } - canvas.insertReorderBarrier(false); + canvas.enableZ(false); } void doFrame(int frameNr) override { int curFrame = frameNr % 150; diff --git a/libs/hwui/tests/common/scenes/ShadowShaderAnimation.cpp b/libs/hwui/tests/common/scenes/ShadowShaderAnimation.cpp index 9f599100200e..0019da5fd80b 100644 --- a/libs/hwui/tests/common/scenes/ShadowShaderAnimation.cpp +++ b/libs/hwui/tests/common/scenes/ShadowShaderAnimation.cpp @@ -29,7 +29,7 @@ public: std::vector<sp<RenderNode> > cards; void createContent(int width, int height, Canvas& canvas) override { canvas.drawColor(0xFFFFFFFF, SkBlendMode::kSrcOver); - canvas.insertReorderBarrier(true); + canvas.enableZ(true); int outset = 50; for (int i = 0; i < 10; i++) { @@ -39,7 +39,7 @@ public: cards.push_back(card); } - canvas.insertReorderBarrier(false); + canvas.enableZ(false); } void doFrame(int frameNr) override { int curFrame = frameNr % 10; diff --git a/libs/hwui/tests/common/scenes/TvApp.cpp b/libs/hwui/tests/common/scenes/TvApp.cpp index bac887053d2f..1b0a07a98b3f 100644 --- a/libs/hwui/tests/common/scenes/TvApp.cpp +++ b/libs/hwui/tests/common/scenes/TvApp.cpp @@ -67,7 +67,7 @@ public: mBg = createBitmapNode(canvas, 0xFF9C27B0, 0, 0, width, height); canvas.drawRenderNode(mBg.get()); - canvas.insertReorderBarrier(true); + canvas.enableZ(true); mSingleBitmap = mAllocator(dp(160), dp(120), kRGBA_8888_SkColorType, [](SkBitmap& skBitmap) { skBitmap.eraseColor(0xFF0000FF); }); @@ -80,7 +80,7 @@ public: mCards.push_back(card); } } - canvas.insertReorderBarrier(false); + canvas.enableZ(false); } void doFrame(int frameNr) override { diff --git a/libs/hwui/tests/macrobench/TestSceneRunner.cpp b/libs/hwui/tests/macrobench/TestSceneRunner.cpp index 801cb7d9e8c5..eda5d2266dcf 100644 --- a/libs/hwui/tests/macrobench/TestSceneRunner.cpp +++ b/libs/hwui/tests/macrobench/TestSceneRunner.cpp @@ -145,7 +145,8 @@ void run(const TestScene::Info& info, const TestScene::Options& opts, for (int i = 0; i < warmupFrameCount; i++) { testContext.waitForVsync(); nsecs_t vsync = systemTime(SYSTEM_TIME_MONOTONIC); - UiFrameInfoBuilder(proxy->frameInfo()).setVsync(vsync, vsync); + UiFrameInfoBuilder(proxy->frameInfo()) + .setVsync(vsync, vsync, UiFrameInfoBuilder::INVALID_VSYNC_ID, std::numeric_limits<int64_t>::max()); proxy->syncAndDrawFrame(); } @@ -165,7 +166,8 @@ void run(const TestScene::Info& info, const TestScene::Options& opts, nsecs_t vsync = systemTime(SYSTEM_TIME_MONOTONIC); { ATRACE_NAME("UI-Draw Frame"); - UiFrameInfoBuilder(proxy->frameInfo()).setVsync(vsync, vsync); + UiFrameInfoBuilder(proxy->frameInfo()) + .setVsync(vsync, vsync, UiFrameInfoBuilder::INVALID_VSYNC_ID, std::numeric_limits<int64_t>::max()); scene->doFrame(i); proxy->syncAndDrawFrame(); } diff --git a/libs/hwui/tests/microbench/DisplayListCanvasBench.cpp b/libs/hwui/tests/microbench/DisplayListCanvasBench.cpp index 4ce6c32470ea..d393c693c774 100644 --- a/libs/hwui/tests/microbench/DisplayListCanvasBench.cpp +++ b/libs/hwui/tests/microbench/DisplayListCanvasBench.cpp @@ -133,14 +133,14 @@ void BM_DisplayListCanvas_basicViewGroupDraw(benchmark::State& benchState) { int clipRestoreCount = canvas->save(SaveFlags::MatrixClip); canvas->clipRect(1, 1, 199, 199, SkClipOp::kIntersect); - canvas->insertReorderBarrier(true); + canvas->enableZ(true); // Draw child loop for (int i = 0; i < benchState.range(0); i++) { canvas->drawRenderNode(child.get()); } - canvas->insertReorderBarrier(false); + canvas->enableZ(false); canvas->restoreToCount(clipRestoreCount); delete canvas->finishRecording(); diff --git a/libs/hwui/tests/scripts/prep_generic.sh b/libs/hwui/tests/scripts/prep_generic.sh index 223bf373c65a..89826ff69463 100755 --- a/libs/hwui/tests/scripts/prep_generic.sh +++ b/libs/hwui/tests/scripts/prep_generic.sh @@ -28,11 +28,17 @@ # performance between different device models. # Fun notes for maintaining this file: -# `expr` can deal with ints > INT32_MAX, but if compares cannot. This is why we use MHz. -# `expr` can sometimes evaluate right-to-left. This is why we use parens. +# $((arithmetic expressions)) can deal with ints > INT32_MAX, but if compares cannot. This is +# why we use MHz. +# $((arithmetic expressions)) can sometimes evaluate right-to-left. This is why we use parens. # Everything below the initial host-check isn't bash - Android uses mksh # mksh allows `\n` in an echo, bash doesn't # can't use `awk` +# can't use `sed` +# can't use `cut` on < L +# can't use `expr` on < L + +ARG_CORES=${1:-big} CPU_TARGET_FREQ_PERCENT=50 GPU_TARGET_FREQ_PERCENT=50 @@ -43,7 +49,7 @@ if [ "`command -v getprop`" == "" ]; then echo "Pushing $0 and running it on device..." dest=/data/local/tmp/`basename $0` adb push $0 ${dest} - adb shell ${dest} + adb shell ${dest} $@ adb shell rm ${dest} exit else @@ -56,7 +62,7 @@ if [ "`command -v getprop`" == "" ]; then fi # require root -if [ "`id -u`" -ne "0" ]; then +if [[ `id` != "uid=0"* ]]; then echo "Not running as root, cannot lock clocks, aborting" exit -1 fi @@ -64,74 +70,175 @@ fi DEVICE=`getprop ro.product.device` MODEL=`getprop ro.product.model` -# Find CPU max frequency, and lock big cores to an available frequency -# that's >= $CPU_TARGET_FREQ_PERCENT% of max. Disable other cores. +if [ "$ARG_CORES" == "big" ]; then + CPU_IDEAL_START_FREQ_KHZ=0 +elif [ "$ARG_CORES" == "little" ]; then + CPU_IDEAL_START_FREQ_KHZ=100000000 ## finding min of max freqs, so start at 100M KHz (100 GHz) +else + echo "Invalid argument \$1 for ARG_CORES, should be 'big' or 'little', but was $ARG_CORES" + exit -1 +fi + +function_core_check() { + if [ "$ARG_CORES" == "big" ]; then + [ $1 -gt $2 ] + elif [ "$ARG_CORES" == "little" ]; then + [ $1 -lt $2 ] + else + echo "Invalid argument \$1 for ARG_CORES, should be 'big' or 'little', but was $ARG_CORES" + exit -1 + fi +} + +function_setup_go() { + if [ -f /d/fpsgo/common/force_onoff ]; then + # Disable fpsgo + echo 0 > /d/fpsgo/common/force_onoff + fpsgoState=`cat /d/fpsgo/common/force_onoff` + if [ "$fpsgoState" != "0" ] && [ "$fpsgoState" != "force off" ]; then + echo "Failed to disable fpsgo" + exit -1 + fi + fi +} + +# Find the min or max (little vs big) of CPU max frequency, and lock cores of the selected type to +# an available frequency that's >= $CPU_TARGET_FREQ_PERCENT% of max. Disable other cores. function_lock_cpu() { CPU_BASE=/sys/devices/system/cpu GOV=cpufreq/scaling_governor + # Options to make clock locking on go devices more sticky. + function_setup_go + # Find max CPU freq, and associated list of available freqs - cpuMaxFreq=0 + cpuIdealFreq=$CPU_IDEAL_START_FREQ_KHZ cpuAvailFreqCmpr=0 cpuAvailFreq=0 enableIndices='' disableIndices='' cpu=0 - while [ -f ${CPU_BASE}/cpu${cpu}/online ]; do - # enable core, so we can find its frequencies - echo 1 > ${CPU_BASE}/cpu${cpu}/online + while [ -d ${CPU_BASE}/cpu${cpu}/cpufreq ]; do + # Try to enable core, so we can find its frequencies. + # Note: In cases where the online file is inaccessible, it represents a + # core which cannot be turned off, so we simply assume it is enabled if + # this command fails. + if [ -f "$CPU_BASE/cpu$cpu/online" ]; then + echo 1 > ${CPU_BASE}/cpu${cpu}/online || true + fi + + # set userspace governor on all CPUs to ensure freq scaling is disabled + echo userspace > ${CPU_BASE}/cpu${cpu}/${GOV} maxFreq=`cat ${CPU_BASE}/cpu$cpu/cpufreq/cpuinfo_max_freq` availFreq=`cat ${CPU_BASE}/cpu$cpu/cpufreq/scaling_available_frequencies` availFreqCmpr=${availFreq// /-} - if [ ${maxFreq} -gt ${cpuMaxFreq} ]; then - # new highest max freq, look for cpus with same max freq and same avail freq list - cpuMaxFreq=${maxFreq} + if (function_core_check $maxFreq $cpuIdealFreq); then + # new min/max of max freq, look for cpus with same max freq and same avail freq list + cpuIdealFreq=${maxFreq} cpuAvailFreq=${availFreq} cpuAvailFreqCmpr=${availFreqCmpr} - if [ -z ${disableIndices} ]; then + if [ -z "$disableIndices" ]; then disableIndices="$enableIndices" else disableIndices="$disableIndices $enableIndices" fi enableIndices=${cpu} - elif [ ${maxFreq} == ${cpuMaxFreq} ] && [ ${availFreqCmpr} == ${cpuAvailFreqCmpr} ]; then + elif [ ${maxFreq} == ${cpuIdealFreq} ] && [ ${availFreqCmpr} == ${cpuAvailFreqCmpr} ]; then enableIndices="$enableIndices $cpu" else - disableIndices="$disableIndices $cpu" + if [ -z "$disableIndices" ]; then + disableIndices="$cpu" + else + disableIndices="$disableIndices $cpu" + fi fi + cpu=$(($cpu + 1)) done + # check that some CPUs will be enabled + if [ -z "$enableIndices" ]; then + echo "Failed to find any $ARG_CORES cores to enable, aborting." + exit -1 + fi + # Chose a frequency to lock to that's >= $CPU_TARGET_FREQ_PERCENT% of max # (below, 100M = 1K for KHz->MHz * 100 for %) - TARGET_FREQ_MHZ=`expr \( ${cpuMaxFreq} \* ${CPU_TARGET_FREQ_PERCENT} \) \/ 100000` + TARGET_FREQ_MHZ=$(( ($cpuIdealFreq * $CPU_TARGET_FREQ_PERCENT) / 100000 )) chosenFreq=0 + chosenFreqDiff=100000000 for freq in ${cpuAvailFreq}; do - freqMhz=`expr ${freq} \/ 1000` + freqMhz=$(( ${freq} / 1000 )) if [ ${freqMhz} -ge ${TARGET_FREQ_MHZ} ]; then - chosenFreq=${freq} - break + newChosenFreqDiff=$(( $freq - $TARGET_FREQ_MHZ )) + if [ $newChosenFreqDiff -lt $chosenFreqDiff ]; then + chosenFreq=${freq} + chosenFreqDiff=$(( $chosenFreq - $TARGET_FREQ_MHZ )) + fi fi done + # Lock wembley clocks using high-priority op code method. + # This block depends on the shell utility awk, which is only available on API 27+ + if [ "$DEVICE" == "wembley" ]; then + # Get list of available frequencies to lock to by parsing the op-code list. + AVAIL_OP_FREQS=`cat /proc/cpufreq/MT_CPU_DVFS_LL/cpufreq_oppidx \ + | awk '{print $2}' \ + | tail -n +3 \ + | while read line; do + echo "${line:1:${#line}-2}" + done` + + # Compute the closest available frequency to the desired frequency, $chosenFreq. + # This assumes the op codes listen in /proc/cpufreq/MT_CPU_DVFS_LL/cpufreq_oppidx are listed + # in order and 0-indexed. + opCode=-1 + opFreq=0 + currOpCode=-1 + for currOpFreq in $AVAIL_OP_FREQS; do + currOpCode=$((currOpCode + 1)) + + prevDiff=$((chosenFreq-opFreq)) + prevDiff=`function_abs $prevDiff` + currDiff=$((chosenFreq-currOpFreq)) + currDiff=`function_abs $currDiff` + if [ $currDiff -lt $prevDiff ]; then + opCode="$currOpCode" + opFreq="$currOpFreq" + fi + done + + echo "$opCode" > /proc/ppm/policy/ut_fix_freq_idx + fi + # enable 'big' CPUs for cpu in ${enableIndices}; do freq=${CPU_BASE}/cpu$cpu/cpufreq - echo 1 > ${CPU_BASE}/cpu${cpu}/online - echo userspace > ${CPU_BASE}/cpu${cpu}/${GOV} + # Try to enable core, so we can find its frequencies. + # Note: In cases where the online file is inaccessible, it represents a + # core which cannot be turned off, so we simply assume it is enabled if + # this command fails. + if [ -f "$CPU_BASE/cpu$cpu/online" ]; then + echo 1 > ${CPU_BASE}/cpu${cpu}/online || true + fi + + # scaling_max_freq must be set before scaling_min_freq echo ${chosenFreq} > ${freq}/scaling_max_freq echo ${chosenFreq} > ${freq}/scaling_min_freq echo ${chosenFreq} > ${freq}/scaling_setspeed + # Give system a bit of time to propagate the change to scaling_setspeed. + sleep 0.1 + # validate setting the freq worked obsCur=`cat ${freq}/scaling_cur_freq` obsMin=`cat ${freq}/scaling_min_freq` obsMax=`cat ${freq}/scaling_max_freq` - if [ obsCur -ne ${chosenFreq} ] || [ obsMin -ne ${chosenFreq} ] || [ obsMax -ne ${chosenFreq} ]; then + if [ "$obsCur" -ne "$chosenFreq" ] || [ "$obsMin" -ne "$chosenFreq" ] || [ "$obsMax" -ne "$chosenFreq" ]; then echo "Failed to set CPU$cpu to $chosenFreq Hz! Aborting..." echo "scaling_cur_freq = $obsCur" echo "scaling_min_freq = $obsMin" @@ -145,8 +252,20 @@ function_lock_cpu() { echo 0 > ${CPU_BASE}/cpu${cpu}/online done - echo "\nLocked CPUs ${enableIndices// /,} to $chosenFreq / $maxFreq KHz" + echo "==================================" + echo "Locked CPUs ${enableIndices// /,} to $chosenFreq / $cpuIdealFreq KHz" echo "Disabled CPUs ${disableIndices// /,}" + echo "==================================" +} + +# Returns the absolute value of the first arg passed to this helper. +function_abs() { + n=$1 + if [ $n -lt 0 ]; then + echo "$((n * -1 ))" + else + echo "$n" + fi } # If we have a Qualcomm GPU, find its max frequency, and lock to @@ -154,12 +273,12 @@ function_lock_cpu() { function_lock_gpu_kgsl() { if [ ! -d /sys/class/kgsl/kgsl-3d0/ ]; then # not kgsl, abort - echo "\nCurrently don't support locking GPU clocks of $MODEL ($DEVICE)" + echo "Currently don't support locking GPU clocks of $MODEL ($DEVICE)" return -1 fi if [ ${DEVICE} == "walleye" ] || [ ${DEVICE} == "taimen" ]; then # Workaround crash - echo "\nUnable to lock GPU clocks of $MODEL ($DEVICE)" + echo "Unable to lock GPU clocks of $MODEL ($DEVICE)" return -1 fi @@ -174,13 +293,13 @@ function_lock_gpu_kgsl() { done # (below, 100M = 1M for MHz * 100 for %) - TARGET_FREQ_MHZ=`expr \( ${gpuMaxFreq} \* ${GPU_TARGET_FREQ_PERCENT} \) \/ 100000000` + TARGET_FREQ_MHZ=$(( (${gpuMaxFreq} * ${GPU_TARGET_FREQ_PERCENT}) / 100000000 )) chosenFreq=${gpuMaxFreq} index=0 chosenIndex=0 for freq in ${gpuAvailFreq}; do - freqMhz=`expr ${freq} \/ 1000000` + freqMhz=$(( ${freq} / 1000000 )) if [ ${freqMhz} -ge ${TARGET_FREQ_MHZ} ] && [ ${chosenFreq} -ge ${freq} ]; then # note avail freq are generally in reverse order, so we don't break out of this loop chosenFreq=${freq} @@ -190,7 +309,7 @@ function_lock_gpu_kgsl() { done lastIndex=$(($index - 1)) - firstFreq=`echo $gpuAvailFreq | cut -d" " -f1` + firstFreq=`function_cut_first_from_space_seperated_list $gpuAvailFreq` if [ ${gpuMaxFreq} != ${firstFreq} ]; then # pwrlevel is index of desired freq among available frequencies, from highest to lowest. @@ -226,24 +345,40 @@ function_lock_gpu_kgsl() { echo "index = $chosenIndex" exit -1 fi - echo "\nLocked GPU to $chosenFreq / $gpuMaxFreq Hz" + echo "Locked GPU to $chosenFreq / $gpuMaxFreq Hz" +} + +# cut is not available on some devices (Nexus 5 running LRX22C). +function_cut_first_from_space_seperated_list() { + list=$1 + + for freq in $list; do + echo $freq + break + done } # kill processes that manage thermals / scaling -stop thermal-engine -stop perfd -stop vendor.thermal-engine -stop vendor.perfd +stop thermal-engine || true +stop perfd || true +stop vendor.thermal-engine || true +stop vendor.perfd || true +setprop vendor.powerhal.init 0 || true +setprop ctl.interface_restart android.hardware.power@1.0::IPower/default || true function_lock_cpu -function_lock_gpu_kgsl +if [ "$DEVICE" -ne "wembley" ]; then + function_lock_gpu_kgsl +else + echo "Unable to lock gpu clocks of $MODEL ($DEVICE)." +fi # Memory bus - hardcoded per-device for now if [ ${DEVICE} == "marlin" ] || [ ${DEVICE} == "sailfish" ]; then echo 13763 > /sys/class/devfreq/soc:qcom,gpubw/max_freq else - echo "\nUnable to lock memory bus of $MODEL ($DEVICE)." + echo "Unable to lock memory bus of $MODEL ($DEVICE)." fi -echo "\n$DEVICE clocks have been locked - to reset, reboot the device\n"
\ No newline at end of file +echo "$DEVICE clocks have been locked - to reset, reboot the device" diff --git a/libs/hwui/tests/unit/CacheManagerTests.cpp b/libs/hwui/tests/unit/CacheManagerTests.cpp index c83a3c88cbdd..edd3e4e4f4d4 100644 --- a/libs/hwui/tests/unit/CacheManagerTests.cpp +++ b/libs/hwui/tests/unit/CacheManagerTests.cpp @@ -26,7 +26,7 @@ using namespace android; using namespace android::uirenderer; using namespace android::uirenderer::renderthread; -static size_t getCacheUsage(GrContext* grContext) { +static size_t getCacheUsage(GrDirectContext* grContext) { size_t cacheUsage; grContext->getResourceCacheUsage(nullptr, &cacheUsage); return cacheUsage; @@ -35,7 +35,7 @@ static size_t getCacheUsage(GrContext* grContext) { RENDERTHREAD_SKIA_PIPELINE_TEST(CacheManager, trimMemory) { int32_t width = DeviceInfo::get()->getWidth(); int32_t height = DeviceInfo::get()->getHeight(); - GrContext* grContext = renderThread.getGrContext(); + GrDirectContext* grContext = renderThread.getGrContext(); ASSERT_TRUE(grContext != nullptr); // create pairs of offscreen render targets and images until we exceed the @@ -47,7 +47,7 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(CacheManager, trimMemory) { sk_sp<SkSurface> surface = SkSurface::MakeRenderTarget(grContext, SkBudgeted::kYes, info); surface->getCanvas()->drawColor(SK_AlphaTRANSPARENT); - grContext->flush(); + grContext->flushAndSubmit(); surfaces.push_back(surface); } diff --git a/libs/hwui/tests/unit/CanvasContextTests.cpp b/libs/hwui/tests/unit/CanvasContextTests.cpp index 28cff5b9b154..1771c3590e10 100644 --- a/libs/hwui/tests/unit/CanvasContextTests.cpp +++ b/libs/hwui/tests/unit/CanvasContextTests.cpp @@ -42,14 +42,3 @@ RENDERTHREAD_TEST(CanvasContext, create) { canvasContext->destroy(); } - -RENDERTHREAD_TEST(CanvasContext, invokeFunctor) { - TestUtils::MockFunctor functor; - CanvasContext::invokeFunctor(renderThread, &functor); - if (Properties::getRenderPipelineType() == RenderPipelineType::SkiaVulkan) { - // we currently don't support OpenGL WebViews on the Vulkan backend - ASSERT_EQ(functor.getLastMode(), DrawGlInfo::kModeProcessNoContext); - } else { - ASSERT_EQ(functor.getLastMode(), DrawGlInfo::kModeProcess); - } -} diff --git a/libs/hwui/tests/unit/FatalTestCanvas.h b/libs/hwui/tests/unit/FatalTestCanvas.h index 1723c2eb4948..76ae0853b477 100644 --- a/libs/hwui/tests/unit/FatalTestCanvas.h +++ b/libs/hwui/tests/unit/FatalTestCanvas.h @@ -81,21 +81,6 @@ public: const SkPaint*) { ADD_FAILURE() << "onDrawImageLattice not expected in this test"; } - void onDrawBitmap(const SkBitmap&, SkScalar dx, SkScalar dy, const SkPaint*) { - ADD_FAILURE() << "onDrawBitmap not expected in this test"; - } - void onDrawBitmapRect(const SkBitmap&, const SkRect*, const SkRect&, const SkPaint*, - SrcRectConstraint) { - ADD_FAILURE() << "onDrawBitmapRect not expected in this test"; - } - void onDrawBitmapNine(const SkBitmap&, const SkIRect& center, const SkRect& dst, - const SkPaint*) { - ADD_FAILURE() << "onDrawBitmapNine not expected in this test"; - } - void onDrawBitmapLattice(const SkBitmap&, const Lattice& lattice, const SkRect& dst, - const SkPaint*) { - ADD_FAILURE() << "onDrawBitmapLattice not expected in this test"; - } void onClipRRect(const SkRRect& rrect, SkClipOp, ClipEdgeStyle) { ADD_FAILURE() << "onClipRRect not expected in this test"; } diff --git a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp index 3632be06c45f..7aa6be8722cf 100644 --- a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp +++ b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp @@ -108,27 +108,27 @@ protected: TEST(RenderNodeDrawable, zReorder) { auto parent = TestUtils::createSkiaNode(0, 0, 100, 100, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { - canvas.insertReorderBarrier(true); - canvas.insertReorderBarrier(false); + canvas.enableZ(true); + canvas.enableZ(false); drawOrderedNode(&canvas, 0, 10.0f); // in reorder=false at this point, so played inorder drawOrderedRect(&canvas, 1); - canvas.insertReorderBarrier(true); + canvas.enableZ(true); drawOrderedNode(&canvas, 6, 2.0f); drawOrderedRect(&canvas, 3); drawOrderedNode(&canvas, 4, 0.0f); drawOrderedRect(&canvas, 5); drawOrderedNode(&canvas, 2, -2.0f); drawOrderedNode(&canvas, 7, 2.0f); - canvas.insertReorderBarrier(false); + canvas.enableZ(false); drawOrderedRect(&canvas, 8); drawOrderedNode(&canvas, 9, -10.0f); // in reorder=false at this point, so played inorder - canvas.insertReorderBarrier(true); // reorder a node ahead of drawrect op + canvas.enableZ(true); // reorder a node ahead of drawrect op drawOrderedRect(&canvas, 11); drawOrderedNode(&canvas, 10, -1.0f); - canvas.insertReorderBarrier(false); - canvas.insertReorderBarrier(true); // test with two empty reorder sections - canvas.insertReorderBarrier(true); - canvas.insertReorderBarrier(false); + canvas.enableZ(false); + canvas.enableZ(true); // test with two empty reorder sections + canvas.enableZ(true); + canvas.enableZ(false); drawOrderedRect(&canvas, 12); }); @@ -1142,7 +1142,7 @@ TEST(ReorderBarrierDrawable, testShadowMatrix) { 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT, [](RenderProperties& props, SkiaRecordingCanvas& canvas) { canvas.translate(TRANSLATE_X, TRANSLATE_Y); - canvas.insertReorderBarrier(true); + canvas.enableZ(true); auto node = TestUtils::createSkiaNode( CASTER_X, CASTER_Y, CASTER_X + CASTER_WIDTH, CASTER_Y + CASTER_HEIGHT, @@ -1152,7 +1152,7 @@ TEST(ReorderBarrierDrawable, testShadowMatrix) { props.mutableOutline().setShouldClip(true); }); canvas.drawRenderNode(node.get()); - canvas.insertReorderBarrier(false); + canvas.enableZ(false); }); // create a canvas not backed by any device/pixels, but with dimensions to avoid quick rejection @@ -1169,7 +1169,7 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaRecordingCanvas, drawVectorDrawable) { class VectorDrawableTestCanvas : public TestCanvasBase { public: VectorDrawableTestCanvas() : TestCanvasBase(CANVAS_WIDTH, CANVAS_HEIGHT) {} - void onDrawBitmapRect(const SkBitmap& bitmap, const SkRect* src, const SkRect& dst, + void onDrawImageRect(const SkImage*, const SkRect* src, const SkRect& dst, const SkPaint* paint, SrcRectConstraint constraint) override { const int index = mDrawCounter++; switch (index) { diff --git a/libs/hwui/tests/unit/RenderNodeTests.cpp b/libs/hwui/tests/unit/RenderNodeTests.cpp index 1cd9bd8ee9d9..c19e1ed6ce75 100644 --- a/libs/hwui/tests/unit/RenderNodeTests.cpp +++ b/libs/hwui/tests/unit/RenderNodeTests.cpp @@ -231,39 +231,41 @@ TEST(RenderNode, multiTreeValidity) { } TEST(RenderNode, releasedCallback) { - class DecRefOnReleased : public GlFunctorLifecycleListener { - public: - explicit DecRefOnReleased(int* refcnt) : mRefCnt(refcnt) {} - void onGlFunctorReleased(Functor* functor) override { *mRefCnt -= 1; } - - private: - int* mRefCnt; - }; - - int refcnt = 0; - sp<DecRefOnReleased> listener(new DecRefOnReleased(&refcnt)); - Functor noopFunctor; + int functor = WebViewFunctor_create( + nullptr, TestUtils::createMockFunctor(RenderMode::OpenGL_ES), RenderMode::OpenGL_ES); auto node = TestUtils::createNode(0, 0, 200, 400, [&](RenderProperties& props, Canvas& canvas) { - refcnt++; - canvas.callDrawGLFunction(&noopFunctor, listener.get()); + canvas.drawWebViewFunctor(functor); + }); + TestUtils::runOnRenderThreadUnmanaged([&] (RenderThread&) { + TestUtils::syncHierarchyPropertiesAndDisplayList(node); }); - TestUtils::syncHierarchyPropertiesAndDisplayList(node); - EXPECT_EQ(1, refcnt); + auto& counts = TestUtils::countsForFunctor(functor); + EXPECT_EQ(1, counts.sync); + EXPECT_EQ(0, counts.destroyed); TestUtils::recordNode(*node, [&](Canvas& canvas) { - refcnt++; - canvas.callDrawGLFunction(&noopFunctor, listener.get()); + canvas.drawWebViewFunctor(functor); }); - EXPECT_EQ(2, refcnt); + EXPECT_EQ(1, counts.sync); + EXPECT_EQ(0, counts.destroyed); - TestUtils::syncHierarchyPropertiesAndDisplayList(node); - EXPECT_EQ(1, refcnt); + TestUtils::runOnRenderThreadUnmanaged([&] (RenderThread&) { + TestUtils::syncHierarchyPropertiesAndDisplayList(node); + }); + EXPECT_EQ(2, counts.sync); + EXPECT_EQ(0, counts.destroyed); + + WebViewFunctor_release(functor); + EXPECT_EQ(2, counts.sync); + EXPECT_EQ(0, counts.destroyed); TestUtils::recordNode(*node, [](Canvas& canvas) {}); - EXPECT_EQ(1, refcnt); - TestUtils::syncHierarchyPropertiesAndDisplayList(node); - EXPECT_EQ(0, refcnt); + TestUtils::runOnRenderThreadUnmanaged([&] (RenderThread&) { + TestUtils::syncHierarchyPropertiesAndDisplayList(node); + }); + EXPECT_EQ(2, counts.sync); + EXPECT_EQ(1, counts.destroyed); } RENDERTHREAD_TEST(RenderNode, prepareTree_nullableDisplayList) { diff --git a/libs/hwui/tests/unit/SkiaCanvasTests.cpp b/libs/hwui/tests/unit/SkiaCanvasTests.cpp index fcc64fdd0be6..f77ca2a8c06c 100644 --- a/libs/hwui/tests/unit/SkiaCanvasTests.cpp +++ b/libs/hwui/tests/unit/SkiaCanvasTests.cpp @@ -73,7 +73,7 @@ TEST(SkiaCanvas, colorSpaceXform) { // Test picture recording. SkPictureRecorder recorder; - SkCanvas* skPicCanvas = recorder.beginRecording(1, 1, NULL, 0); + SkCanvas* skPicCanvas = recorder.beginRecording(1, 1); SkiaCanvas picCanvas(skPicCanvas); picCanvas.drawBitmap(*adobeBitmap, 0, 0, nullptr); sk_sp<SkPicture> picture = recorder.finishRecordingAsPicture(); @@ -104,7 +104,7 @@ TEST(SkiaCanvas, captureCanvasState) { // Create a picture canvas. SkPictureRecorder recorder; - SkCanvas* skPicCanvas = recorder.beginRecording(1, 1, NULL, 0); + SkCanvas* skPicCanvas = recorder.beginRecording(1, 1); SkiaCanvas picCanvas(skPicCanvas); state = picCanvas.captureCanvasState(); diff --git a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp index d08aea668b2a..74a565439f85 100644 --- a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp +++ b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp @@ -48,7 +48,10 @@ TEST(SkiaDisplayList, reset) { SkCanvas dummyCanvas; RenderNodeDrawable drawable(nullptr, &dummyCanvas); skiaDL->mChildNodes.emplace_back(nullptr, &dummyCanvas); - GLFunctorDrawable functorDrawable(nullptr, nullptr, &dummyCanvas); + int functor1 = WebViewFunctor_create( + nullptr, TestUtils::createMockFunctor(RenderMode::OpenGL_ES), RenderMode::OpenGL_ES); + GLFunctorDrawable functorDrawable{functor1, &dummyCanvas}; + WebViewFunctor_release(functor1); skiaDL->mChildFunctors.push_back(&functorDrawable); skiaDL->mMutableImages.push_back(nullptr); skiaDL->appendVD(nullptr); @@ -97,16 +100,13 @@ TEST(SkiaDisplayList, syncContexts) { SkiaDisplayList skiaDL; SkCanvas dummyCanvas; - TestUtils::MockFunctor functor; - GLFunctorDrawable functorDrawable(&functor, nullptr, &dummyCanvas); - skiaDL.mChildFunctors.push_back(&functorDrawable); - int functor2 = WebViewFunctor_create( + int functor1 = WebViewFunctor_create( nullptr, TestUtils::createMockFunctor(RenderMode::OpenGL_ES), RenderMode::OpenGL_ES); - auto& counts = TestUtils::countsForFunctor(functor2); + auto& counts = TestUtils::countsForFunctor(functor1); skiaDL.mChildFunctors.push_back( - skiaDL.allocateDrawable<GLFunctorDrawable>(functor2, &dummyCanvas)); - WebViewFunctor_release(functor2); + skiaDL.allocateDrawable<GLFunctorDrawable>(functor1, &dummyCanvas)); + WebViewFunctor_release(functor1); SkRect bounds = SkRect::MakeWH(200, 200); VectorDrawableRoot vectorDrawable(new VectorDrawable::Group()); @@ -120,7 +120,6 @@ TEST(SkiaDisplayList, syncContexts) { }); }); - EXPECT_EQ(functor.getLastMode(), DrawGlInfo::kModeSync); EXPECT_EQ(counts.sync, 1); EXPECT_EQ(counts.destroyed, 0); EXPECT_EQ(vectorDrawable.mutateProperties()->getBounds(), bounds); diff --git a/libs/hwui/tests/unit/TypefaceTests.cpp b/libs/hwui/tests/unit/TypefaceTests.cpp index 1a09b1c52d8a..5d2aa2ff83c9 100644 --- a/libs/hwui/tests/unit/TypefaceTests.cpp +++ b/libs/hwui/tests/unit/TypefaceTests.cpp @@ -31,10 +31,12 @@ using namespace android; namespace { -constexpr char kRobotoRegular[] = "/system/fonts/Roboto-Regular.ttf"; -constexpr char kRobotoBold[] = "/system/fonts/Roboto-Bold.ttf"; -constexpr char kRobotoItalic[] = "/system/fonts/Roboto-Italic.ttf"; -constexpr char kRobotoBoldItalic[] = "/system/fonts/Roboto-BoldItalic.ttf"; +constexpr char kRobotoVariable[] = "/system/fonts/Roboto-Regular.ttf"; + +constexpr char kRegularFont[] = "/system/fonts/NotoSerif-Regular.ttf"; +constexpr char kBoldFont[] = "/system/fonts/NotoSerif-Bold.ttf"; +constexpr char kItalicFont[] = "/system/fonts/NotoSerif-Italic.ttf"; +constexpr char kBoldItalicFont[] = "/system/fonts/NotoSerif-BoldItalic.ttf"; void unmap(const void* ptr, void* context) { void* p = const_cast<void*>(ptr); @@ -57,7 +59,7 @@ std::shared_ptr<minikin::FontFamily> buildFamily(const char* fileName) { std::shared_ptr<minikin::MinikinFont> font = std::make_shared<MinikinFontSkia>(std::move(typeface), data, st.st_size, fileName, 0, std::vector<minikin::FontVariation>()); - std::vector<minikin::Font> fonts; + std::vector<std::shared_ptr<minikin::Font>> fonts; fonts.push_back(minikin::Font::Builder(font).build()); return std::make_shared<minikin::FontFamily>(std::move(fonts)); } @@ -68,7 +70,7 @@ std::vector<std::shared_ptr<minikin::FontFamily>> makeSingleFamlyVector(const ch TEST(TypefaceTest, resolveDefault_and_setDefaultTest) { std::unique_ptr<Typeface> regular(Typeface::createFromFamilies( - makeSingleFamlyVector(kRobotoRegular), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)); + makeSingleFamlyVector(kRobotoVariable), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)); EXPECT_EQ(regular.get(), Typeface::resolveDefault(regular.get())); // Keep the original to restore it later. @@ -347,71 +349,71 @@ TEST(TypefaceTest, createFromFamilies_Single) { // In Java, new // Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(false).build(); std::unique_ptr<Typeface> regular( - Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoRegular), 400, false)); + Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 400, false)); EXPECT_EQ(400, regular->fStyle.weight()); EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant()); EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle); // In Java, new - // Typeface.Builder("Roboto-Bold.ttf").setWeight(700).setItalic(false).build(); + // Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(false).build(); std::unique_ptr<Typeface> bold( - Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoBold), 700, false)); + Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 700, false)); EXPECT_EQ(700, bold->fStyle.weight()); EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); // In Java, new - // Typeface.Builder("Roboto-Italic.ttf").setWeight(400).setItalic(true).build(); + // Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(true).build(); std::unique_ptr<Typeface> italic( - Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoItalic), 400, true)); + Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 400, true)); EXPECT_EQ(400, italic->fStyle.weight()); EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); // In Java, // new - // Typeface.Builder("Roboto-BoldItalic.ttf").setWeight(700).setItalic(true).build(); + // Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(true).build(); std::unique_ptr<Typeface> boldItalic( - Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoBoldItalic), 700, true)); + Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 700, true)); EXPECT_EQ(700, boldItalic->fStyle.weight()); EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); // In Java, // new - // Typeface.Builder("Roboto-BoldItalic.ttf").setWeight(1100).setItalic(false).build(); + // Typeface.Builder("Roboto-Regular.ttf").setWeight(1100).setItalic(false).build(); std::unique_ptr<Typeface> over1000( - Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoBold), 1100, false)); + Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 1100, false)); EXPECT_EQ(1000, over1000->fStyle.weight()); EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->fStyle.slant()); EXPECT_EQ(Typeface::kBold, over1000->fAPIStyle); } TEST(TypefaceTest, createFromFamilies_Single_resolveByTable) { - // In Java, new Typeface.Builder("Roboto-Regular.ttf").build(); + // In Java, new Typeface.Builder("Family-Regular.ttf").build(); std::unique_ptr<Typeface> regular(Typeface::createFromFamilies( - makeSingleFamlyVector(kRobotoRegular), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)); + makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)); EXPECT_EQ(400, regular->fStyle.weight()); EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant()); EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle); - // In Java, new Typeface.Builder("Roboto-Bold.ttf").build(); + // In Java, new Typeface.Builder("Family-Bold.ttf").build(); std::unique_ptr<Typeface> bold(Typeface::createFromFamilies( - makeSingleFamlyVector(kRobotoBold), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)); + makeSingleFamlyVector(kBoldFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)); EXPECT_EQ(700, bold->fStyle.weight()); EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); - // In Java, new Typeface.Builder("Roboto-Italic.ttf").build(); + // In Java, new Typeface.Builder("Family-Italic.ttf").build(); std::unique_ptr<Typeface> italic(Typeface::createFromFamilies( - makeSingleFamlyVector(kRobotoItalic), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)); + makeSingleFamlyVector(kItalicFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)); EXPECT_EQ(400, italic->fStyle.weight()); EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); - // In Java, new Typeface.Builder("Roboto-BoldItalic.ttf").build(); + // In Java, new Typeface.Builder("Family-BoldItalic.ttf").build(); std::unique_ptr<Typeface> boldItalic( - Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoBoldItalic), + Typeface::createFromFamilies(makeSingleFamlyVector(kBoldItalicFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)); EXPECT_EQ(700, boldItalic->fStyle.weight()); EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); @@ -420,8 +422,8 @@ TEST(TypefaceTest, createFromFamilies_Single_resolveByTable) { TEST(TypefaceTest, createFromFamilies_Family) { std::vector<std::shared_ptr<minikin::FontFamily>> families = { - buildFamily(kRobotoRegular), buildFamily(kRobotoBold), buildFamily(kRobotoItalic), - buildFamily(kRobotoBoldItalic)}; + buildFamily(kRegularFont), buildFamily(kBoldFont), buildFamily(kItalicFont), + buildFamily(kBoldItalicFont)}; std::unique_ptr<Typeface> typeface(Typeface::createFromFamilies( std::move(families), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)); EXPECT_EQ(400, typeface->fStyle.weight()); @@ -430,7 +432,7 @@ TEST(TypefaceTest, createFromFamilies_Family) { TEST(TypefaceTest, createFromFamilies_Family_withoutRegular) { std::vector<std::shared_ptr<minikin::FontFamily>> families = { - buildFamily(kRobotoBold), buildFamily(kRobotoItalic), buildFamily(kRobotoBoldItalic)}; + buildFamily(kBoldFont), buildFamily(kItalicFont), buildFamily(kBoldItalicFont)}; std::unique_ptr<Typeface> typeface(Typeface::createFromFamilies( std::move(families), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)); EXPECT_EQ(700, typeface->fStyle.weight()); diff --git a/libs/hwui/utils/Blur.h b/libs/hwui/utils/Blur.h index d6b41b83def8..6b822f01e25c 100644 --- a/libs/hwui/utils/Blur.h +++ b/libs/hwui/utils/Blur.h @@ -26,9 +26,9 @@ namespace uirenderer { class Blur { public: // If radius > 0, return the corresponding sigma, else return 0 - ANDROID_API static float convertRadiusToSigma(float radius); + static float convertRadiusToSigma(float radius); // If sigma > 0.5, return the corresponding radius, else return 0 - ANDROID_API static float convertSigmaToRadius(float sigma); + static float convertSigmaToRadius(float sigma); // If the original radius was on an integer boundary then after the sigma to // radius conversion a small rounding error may be introduced. This function // accounts for that error and snaps to the appropriate integer boundary. diff --git a/libs/hwui/utils/Color.cpp b/libs/hwui/utils/Color.cpp index 71a27ced2e09..87512f0354c8 100644 --- a/libs/hwui/utils/Color.cpp +++ b/libs/hwui/utils/Color.cpp @@ -16,8 +16,8 @@ #include "Color.h" -#include <utils/Log.h> #include <ui/ColorSpace.h> +#include <utils/Log.h> #ifdef __ANDROID__ // Layoutlib does not support hardware buffers or native windows #include <android/hardware_buffer.h> @@ -26,6 +26,7 @@ #include <algorithm> #include <cmath> +#include <Properties.h> namespace android { namespace uirenderer { @@ -72,46 +73,34 @@ SkImageInfo BufferDescriptionToImageInfo(const AHardwareBuffer_Desc& bufferDesc, sk_sp<SkColorSpace> colorSpace) { return createImageInfo(bufferDesc.width, bufferDesc.height, bufferDesc.format, colorSpace); } -#endif -android::PixelFormat ColorTypeToPixelFormat(SkColorType colorType) { +uint32_t ColorTypeToBufferFormat(SkColorType colorType) { switch (colorType) { case kRGBA_8888_SkColorType: - return PIXEL_FORMAT_RGBA_8888; + return AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; case kRGBA_F16_SkColorType: - return PIXEL_FORMAT_RGBA_FP16; + return AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT; case kRGB_565_SkColorType: - return PIXEL_FORMAT_RGB_565; + return AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM; case kRGB_888x_SkColorType: - return PIXEL_FORMAT_RGBX_8888; + return AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM; case kRGBA_1010102_SkColorType: - return PIXEL_FORMAT_RGBA_1010102; + return AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM; case kARGB_4444_SkColorType: - return PIXEL_FORMAT_RGBA_4444; + // Hardcoding the value from android::PixelFormat + static constexpr uint64_t kRGBA4444 = 7; + return kRGBA4444; default: ALOGV("Unsupported colorType: %d, return RGBA_8888 by default", (int)colorType); - return PIXEL_FORMAT_RGBA_8888; - } -} - -SkColorType PixelFormatToColorType(android::PixelFormat format) { - switch (format) { - case PIXEL_FORMAT_RGBX_8888: return kRGB_888x_SkColorType; - case PIXEL_FORMAT_RGBA_8888: return kRGBA_8888_SkColorType; - case PIXEL_FORMAT_RGBA_FP16: return kRGBA_F16_SkColorType; - case PIXEL_FORMAT_RGB_565: return kRGB_565_SkColorType; - case PIXEL_FORMAT_RGBA_1010102: return kRGBA_1010102_SkColorType; - case PIXEL_FORMAT_RGBA_4444: return kARGB_4444_SkColorType; - default: - ALOGV("Unsupported PixelFormat: %d, return kUnknown_SkColorType by default", format); - return kUnknown_SkColorType; + return AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; } } +#endif namespace { static constexpr skcms_TransferFunction k2Dot6 = {2.6f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; -// Skia's SkNamedGamut::kDCIP3 is based on a white point of D65. This gamut +// Skia's SkNamedGamut::kDisplayP3 is based on a white point of D65. This gamut // matches the white point used by ColorSpace.Named.DCIP3. static constexpr skcms_Matrix3x3 kDCIP3 = {{ {0.486143, 0.323835, 0.154234}, @@ -180,7 +169,7 @@ android_dataspace ColorSpaceToADataSpace(SkColorSpace* colorSpace, SkColorType c } } - if (nearlyEqual(fn, SkNamedTransferFn::kSRGB) && nearlyEqual(gamut, SkNamedGamut::kDCIP3)) { + if (nearlyEqual(fn, SkNamedTransferFn::kSRGB) && nearlyEqual(gamut, SkNamedGamut::kDisplayP3)) { return HAL_DATASPACE_DISPLAY_P3; } @@ -221,7 +210,7 @@ sk_sp<SkColorSpace> DataSpaceToColorSpace(android_dataspace dataspace) { gamut = SkNamedGamut::kRec2020; break; case HAL_DATASPACE_STANDARD_DCI_P3: - gamut = SkNamedGamut::kDCIP3; + gamut = SkNamedGamut::kDisplayP3; break; case HAL_DATASPACE_STANDARD_ADOBE_RGB: gamut = SkNamedGamut::kAdobeRGB; @@ -356,5 +345,23 @@ SkColor LabToSRGB(const Lab& lab, SkAlpha alpha) { static_cast<uint8_t>(rgb.b * 255)); } +skcms_TransferFunction GetPQSkTransferFunction(float sdr_white_level) { + if (sdr_white_level <= 0.f) { + sdr_white_level = Properties::defaultSdrWhitePoint; + } + // The generic PQ transfer function produces normalized luminance values i.e. + // the range 0-1 represents 0-10000 nits for the reference display, but we + // want to map 1.0 to |sdr_white_level| nits so we need to scale accordingly. + const double w = 10000. / sdr_white_level; + // Distribute scaling factor W by scaling A and B with X ^ (1/F): + // ((A + Bx^C) / (D + Ex^C))^F * W = ((A + Bx^C) / (D + Ex^C) * W^(1/F))^F + // See https://crbug.com/1058580#c32 for discussion. + skcms_TransferFunction fn = SkNamedTransferFn::kPQ; + const double ws = pow(w, 1. / fn.f); + fn.a = ws * fn.a; + fn.b = ws * fn.b; + return fn; +} + } // namespace uirenderer } // namespace android diff --git a/libs/hwui/utils/Color.h b/libs/hwui/utils/Color.h index a76f7e499c37..1654072fd264 100644 --- a/libs/hwui/utils/Color.h +++ b/libs/hwui/utils/Color.h @@ -16,14 +16,12 @@ #ifndef COLOR_H #define COLOR_H -#include <math.h> -#include <cutils/compiler.h> -#include <system/graphics.h> -#include <ui/PixelFormat.h> - #include <SkColor.h> #include <SkColorSpace.h> #include <SkImageInfo.h> +#include <cutils/compiler.h> +#include <math.h> +#include <system/graphics.h> struct ANativeWindow_Buffer; struct AHardwareBuffer_Desc; @@ -93,15 +91,14 @@ static constexpr float EOCF_sRGB(float srgb) { } #ifdef __ANDROID__ // Layoutlib does not support hardware buffers or native windows -ANDROID_API SkImageInfo ANativeWindowToImageInfo(const ANativeWindow_Buffer& buffer, +SkImageInfo ANativeWindowToImageInfo(const ANativeWindow_Buffer& buffer, sk_sp<SkColorSpace> colorSpace); SkImageInfo BufferDescriptionToImageInfo(const AHardwareBuffer_Desc& bufferDesc, sk_sp<SkColorSpace> colorSpace); -#endif -android::PixelFormat ColorTypeToPixelFormat(SkColorType colorType); -ANDROID_API SkColorType PixelFormatToColorType(android::PixelFormat format); +uint32_t ColorTypeToBufferFormat(SkColorType colorType); +#endif ANDROID_API sk_sp<SkColorSpace> DataSpaceToColorSpace(android_dataspace dataspace); @@ -129,6 +126,7 @@ struct Lab { Lab sRGBToLab(SkColor color); SkColor LabToSRGB(const Lab& lab, SkAlpha alpha); +skcms_TransferFunction GetPQSkTransferFunction(float sdr_white_level = 0.f); } /* namespace uirenderer */ } /* namespace android */ diff --git a/libs/hwui/utils/MathUtils.h b/libs/hwui/utils/MathUtils.h index cc8d83f10d43..62bf39ca8a7a 100644 --- a/libs/hwui/utils/MathUtils.h +++ b/libs/hwui/utils/MathUtils.h @@ -31,7 +31,9 @@ public: * Check for floats that are close enough to zero. */ inline static bool isZero(float value) { - return (value >= -NON_ZERO_EPSILON) && (value <= NON_ZERO_EPSILON); + // Using fabsf is more performant as ARM computes + // fabsf in a single instruction. + return fabsf(value) <= NON_ZERO_EPSILON; } inline static bool isOne(float value) { diff --git a/libs/hwui/GlFunctorLifecycleListener.h b/libs/hwui/utils/NdkUtils.cpp index 5adc46961c8b..de6274ee5bcc 100644 --- a/libs/hwui/GlFunctorLifecycleListener.h +++ b/libs/hwui/utils/NdkUtils.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * 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. @@ -14,19 +14,19 @@ * limitations under the License. */ -#pragma once - -#include <utils/Functor.h> -#include <utils/RefBase.h> +#include <utils/NdkUtils.h> namespace android { namespace uirenderer { -class GlFunctorLifecycleListener : public VirtualLightRefBase { -public: - virtual ~GlFunctorLifecycleListener() {} - virtual void onGlFunctorReleased(Functor* functor) = 0; -}; +UniqueAHardwareBuffer allocateAHardwareBuffer(const AHardwareBuffer_Desc& desc) { + AHardwareBuffer* buffer; + if (AHardwareBuffer_allocate(&desc, &buffer) != 0) { + return nullptr; + } else { + return UniqueAHardwareBuffer{buffer}; + } +} } // namespace uirenderer } // namespace android diff --git a/libs/hwui/utils/NdkUtils.h b/libs/hwui/utils/NdkUtils.h new file mode 100644 index 000000000000..f218eb2ff404 --- /dev/null +++ b/libs/hwui/utils/NdkUtils.h @@ -0,0 +1,38 @@ +/* + * 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. + */ + +#pragma once + +#include <android/hardware_buffer.h> + +#include <memory> + +namespace android { +namespace uirenderer { + +// Deleter for an AHardwareBuffer, to be passed to an std::unique_ptr. +struct AHardwareBuffer_deleter { + void operator()(AHardwareBuffer* ahb) const { AHardwareBuffer_release(ahb); } +}; + +using UniqueAHardwareBuffer = std::unique_ptr<AHardwareBuffer, AHardwareBuffer_deleter>; + +// Allocates a UniqueAHardwareBuffer with the provided buffer description. +// Returns nullptr if allocation did not succeed. +UniqueAHardwareBuffer allocateAHardwareBuffer(const AHardwareBuffer_Desc& desc); + +} // namespace uirenderer +} // namespace android diff --git a/libs/hwui/utils/VectorDrawableUtils.h b/libs/hwui/utils/VectorDrawableUtils.h index 4be48fb942fc..4f63959165db 100644 --- a/libs/hwui/utils/VectorDrawableUtils.h +++ b/libs/hwui/utils/VectorDrawableUtils.h @@ -28,10 +28,10 @@ namespace uirenderer { class VectorDrawableUtils { public: - ANDROID_API static bool canMorph(const PathData& morphFrom, const PathData& morphTo); - ANDROID_API static bool interpolatePathData(PathData* outData, const PathData& morphFrom, + static bool canMorph(const PathData& morphFrom, const PathData& morphTo); + static bool interpolatePathData(PathData* outData, const PathData& morphFrom, const PathData& morphTo, float fraction); - ANDROID_API static void verbsToPath(SkPath* outPath, const PathData& data); + static void verbsToPath(SkPath* outPath, const PathData& data); static void interpolatePaths(PathData* outPathData, const PathData& from, const PathData& to, float fraction); }; diff --git a/libs/usb/tests/AccessoryChat/AndroidManifest.xml b/libs/usb/tests/AccessoryChat/AndroidManifest.xml index 6667ebaa4d49..b93eeab11324 100644 --- a/libs/usb/tests/AccessoryChat/AndroidManifest.xml +++ b/libs/usb/tests/AccessoryChat/AndroidManifest.xml @@ -15,26 +15,28 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.accessorychat"> + package="com.android.accessorychat"> - <uses-feature android:name="android.hardware.usb.accessory" /> + <uses-feature android:name="android.hardware.usb.accessory"/> <application android:label="Accessory Chat"> - <activity android:name="AccessoryChat" android:label="Accessory Chat"> + <activity android:name="AccessoryChat" + android:label="Accessory Chat" + android:exported="true"> <intent-filter> - <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.DEFAULT" /> - <category android:name="android.intent.category.LAUNCHER" /> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.DEFAULT"/> + <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> <intent-filter> - <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" /> + <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"/> </intent-filter> <meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" - android:resource="@xml/accessory_filter" /> + android:resource="@xml/accessory_filter"/> </activity> </application> - <uses-sdk android:minSdkVersion="12" /> + <uses-sdk android:minSdkVersion="12"/> </manifest> |