diff options
Diffstat (limited to 'libs')
778 files changed, 85766 insertions, 3671 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..96e0559b0df6 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -12,18 +12,124 @@ // 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.appcompat_appcompat", + "androidx.arch.core_core-runtime", + "androidx.dynamicanimation_dynamicanimation", + "kotlinx-coroutines-android", + "kotlinx-coroutines-core", + "iconloader_base", + "jsr330", + "protolog-lib", + "SettingsLib", + "WindowManager-Shell-proto", + "jsr330" + ], + kotlincflags: ["-Xjvm-default=enable"], manifest: "AndroidManifest.xml", - platform_apis: true, - sdk_version: "current", - min_sdk_version: "system_current", + min_sdk_version: "26", } diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml index ea8a5c305029..c0bc73dcbd47 100644 --- a/libs/WindowManager/Shell/AndroidManifest.xml +++ b/libs/WindowManager/Shell/AndroidManifest.xml @@ -17,4 +17,5 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.wm.shell"> + <uses-permission android:name="android.permission.ROTATE_SURFACE_FLINGER" /> </manifest> 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/bubble_dismiss_circle.xml b/libs/WindowManager/Shell/res/drawable/bubble_dismiss_circle.xml new file mode 100644 index 000000000000..2104be48d1d9 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/bubble_dismiss_circle.xml @@ -0,0 +1,28 @@ +<!-- + ~ 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. + --> +<!-- + The transparent circle outline that encircles the bubbles when they're in the dismiss target. +--> +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="oval"> + + <stroke + android:width="1dp" + android:color="#66FFFFFF" /> + + <solid android:color="#B3000000" /> +</shape>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/bubble_dismiss_icon.xml b/libs/WindowManager/Shell/res/drawable/bubble_dismiss_icon.xml new file mode 100644 index 000000000000..ff8feded11ab --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/bubble_dismiss_icon.xml @@ -0,0 +1,26 @@ +<!-- + ~ 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. + --> +<!-- The 'X' bubble dismiss icon. This is just ic_close with a stroke. --> +<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" + android:strokeColor="#FF000000"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/bubble_ic_create_bubble.xml b/libs/WindowManager/Shell/res/drawable/bubble_ic_create_bubble.xml new file mode 100644 index 000000000000..920671a24204 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/bubble_ic_create_bubble.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="20dp" + android:height="20dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="#FF000000" + android:pathData="M23,5v8h-2V5H3v14h10v2v0H3c-1.1,0 -2,-0.9 -2,-2V5c0,-1.1 0.9,-2 2,-2h18C22.1,3 23,3.9 23,5zM10,8v2.59L5.71,6.29L4.29,7.71L8.59,12H6v2h6V8H10zM19,15c-1.66,0 -3,1.34 -3,3s1.34,3 3,3s3,-1.34 3,-3S20.66,15 19,15z"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/bubble_ic_empty_overflow_dark.xml b/libs/WindowManager/Shell/res/drawable/bubble_ic_empty_overflow_dark.xml new file mode 100644 index 000000000000..8f8f1b664692 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/bubble_ic_empty_overflow_dark.xml @@ -0,0 +1,162 @@ +<!-- +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="412dp" + android:height="300dp" + android:viewportWidth="412" + android:viewportHeight="300"> + <group> + <clip-path + android:pathData="M206,150m-150,0a150,150 0,1 1,300 0a150,150 0,1 1,-300 0"/> + <path + android:pathData="M296,105.2h-9.6l-3.1,-2.5l-3.1,2.5H116c-1.7,0 -3,1.3 -3,3v111.7c0,1.7 1.3,3 3,3h180c1.7,0 3,-1.3 3,-3V108.2C299,106.6 297.7,105.2 296,105.2C296,105.2 296,105.2 296,105.2z" + android:fillColor="#3C4043"/> + <path + android:strokeWidth="1" + android:pathData="M252.4,85.3m-12.4,0a12.4,12.4 0,1 1,24.8 0a12.4,12.4 0,1 1,-24.8 0" + android:fillColor="#D2E3FC" + android:strokeColor="#4285F4"/> + <path + android:pathData="M261.9,95.7m-4.5,0a4.5,4.5 0,1 1,9 0a4.5,4.5 0,1 1,-9 0" + android:fillColor="#4285F4"/> + <path + android:strokeWidth="1" + android:pathData="M160.6,85.3m-12.4,0a12.4,12.4 0,1 1,24.8 0a12.4,12.4 0,1 1,-24.8 0" + android:fillColor="#FAD2CF" + android:strokeColor="#EA4335"/> + <path + android:pathData="M170.1,95.7m-4.6,0a4.6,4.6 0,1 1,9.2 0a4.6,4.6 0,1 1,-9.2 0" + android:fillColor="#EA4335"/> + <path + android:strokeWidth="1" + android:pathData="M192.1,85.3m-12.4,0a12.4,12.4 0,1 1,24.8 0a12.4,12.4 0,1 1,-24.8 0" + android:fillColor="#FEEFC3" + android:strokeColor="#FBBC04"/> + <path + android:strokeWidth="1" + android:pathData="M221.8,85.4m-12.4,0a12.4,12.4 0,1 1,24.8 0a12.4,12.4 0,1 1,-24.8 0" + android:fillColor="#CEEAD6" + android:strokeColor="#34A853"/> + <path + android:pathData="M201.6,95.7m-4.6,0a4.6,4.6 0,1 1,9.2 0a4.6,4.6 0,1 1,-9.2 0" + android:fillColor="#FBBC04"/> + <path + android:pathData="M231.4,95.7m-4.6,0a4.6,4.6 0,1 1,9.2 0a4.6,4.6 0,1 1,-9.2 0" + android:fillColor="#34A853"/> + <path + android:pathData="M282.8,85.3m-12.4,0a12.4,12.4 0,1 1,24.8 0a12.4,12.4 0,1 1,-24.8 0" + android:fillColor="#3C4043"/> + <path + android:pathData="M278.7,85.7m-1.2,0a1.2,1.2 0,1 1,2.4 0a1.2,1.2 0,1 1,-2.4 0" + android:fillColor="#D2E3FC"/> + <path + android:pathData="M282.8,85.7m-1.2,0a1.2,1.2 0,1 1,2.4 0a1.2,1.2 0,1 1,-2.4 0" + android:fillColor="#D2E3FC"/> + <path + android:pathData="M286.9,85.7m-1.2,0a1.2,1.2 0,1 1,2.4 0a1.2,1.2 0,1 1,-2.4 0" + android:fillColor="#D2E3FC"/> + <path + android:pathData="M129.2,72.9h-3.4l0.3,1c1,-0.3 2.1,-0.4 3.2,-0.4v-1V72.9zM122.6,74.8c-0.5,0.3 -1,0.6 -1.4,1l0,0l0,0l0,0l0,0h-0.6l0,0l0,0l0,0l0,0l0,0l0,0c-0.2,0.2 -0.3,0.3 -0.4,0.5l0.8,0.7c0.7,-0.8 1.5,-1.5 2.4,-2.1l-0.5,-0.8L122.6,74.8zM118,80L118,80L118,80L118,80L118,80L118,80L118,80L118,80c-0.5,1 -0.8,2 -1,3l1,0.2c0.2,-1 0.5,-2 1,-3L118,80zM117.8,86.7l-1,0.1c0.1,0.6 0.2,1.1 0.3,1.7l0,0l0,0h0.1l0,0l0,0l0,0l0,0c0.1,0.5 0.3,0.9 0.5,1.4l0.9,-0.4c-0.4,-1 -0.7,-2 -0.8,-3.1L117.8,86.7zM120.2,92.5l-0.8,0.6l0.2,0.3l0,0l0,0l0,0l0,0h0.3l0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0l0.4,0.4l0,0l0,0l0,0l0,0h0.1l0,0l0,0l0,0l0,0l0,0h0.5l0,0l0,0l0,0l0,0l0,0l0,0l0.6,0.4l0.6,-0.8c-0.9,-0.6 -1.7,-1.4 -2.3,-2.2L120.2,92.5zM125.4,96.2l-0.3,0.9c1.1,0.4 2.2,0.6 3.4,0.7l0.1,-1C127.5,96.7 126.4,96.5 125.4,96.2zM134.7,95.4c-0.9,0.5 -2,0.9 -3,1.1l0.2,1h0.4c1,-0.3 2,-0.6 2.9,-1.2l-0.5,-0.9L134.7,95.4zM139.2,90.9c-0.5,0.9 -1.2,1.8 -1.9,2.5l0.7,0.7v-0.1h0.2l0,0l0,0c0.7,-0.7 1.3,-1.6 1.8,-2.4l-0.9,-0.5L139.2,90.9zM141.6,84.7h-1c0,0.2 0,0.4 0,0.6c0,0.9 -0.1,1.7 -0.3,2.6l1,0.2c0.1,-0.4 0.2,-0.8 0.2,-1.2l0,0v-0.1l0,0v-0.1l0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0c0,-0.2 0,-0.3 0,-0.5l0,0L141.6,84.7zM139.3,78.2l-0.8,0.6c0.6,0.9 1.1,1.8 1.5,2.8l0.9,-0.3c-0.1,-0.2 -0.2,-0.4 -0.2,-0.7l0,0l0,0h-0.1l0,0l0,0l0,0l0,0l0,0l0,0c-0.3,-0.7 -0.7,-1.4 -1.1,-2l0,0l0,0l0,0l0,0l0,0l0,0l0,0L139.3,78.2zM134,73.9l-0.4,0.9c1,0.4 1.9,1 2.7,1.6l0.6,-0.8l0,0l0,0l0,0l0,0l0,0c-0.3,-0.3 -0.7,-0.5 -1,-0.7l0,0h-0.1h-0.6c-0.4,-0.2 -0.8,-0.4 -1.2,-0.6L134,73.9zM129.2,72.9v1c0.4,0 0.9,0 1.3,0.1l0.1,-1l-0.9,-0.1L129.2,72.9L129.2,72.9z" + android:fillColor="#34A853"/> + <path + android:pathData="M206,252m-11.7,0a11.7,11.7 0,1 1,23.4 0a11.7,11.7 0,1 1,-23.4 0" + android:fillColor="#F1F3F4"/> + <path + android:pathData="M201.7,247.7L210.3,256.3" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#202124" + android:strokeLineCap="round"/> + <path + android:pathData="M210.3,247.7L201.7,256.3" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#202124" + android:strokeLineCap="round"/> + <path + android:strokeWidth="1" + android:pathData="M205.3,221.9m-10.4,0a10.4,10.4 0,1 1,20.8 0a10.4,10.4 0,1 1,-20.8 0" + android:fillColor="#CEEAD6" + android:strokeColor="#34A853"/> + <path + android:pathData="M481.4,292.2c48,58.3 119.8,125.8 58.6,162.9c-38.7,23.5 -53.9,24 -98.3,33.2c-43.8,9.1 -93.6,-89.8 -101.1,-134.5C329.6,288.6 452.6,257.2 481.4,292.2z" + android:fillColor="#D2E3FC"/> + <path + android:pathData="M458.2,320.7l-21.1,-71.4L400.5,193c-2.7,-5.1 -1.2,-11.4 3.5,-14.7l0,0c2.8,-2 6.6,-1.5 8.8,1.1c0,0 40.6,38.4 53.2,61.1l81.5,134.8l-69.9,-39.1L458.2,320.7z" + android:fillColor="#D2E3FC"/> + <path + android:pathData="M403.8,184.8l5.4,6.9c1.2,1.5 3.3,1.9 4.9,0.9l3,-1.8" + android:strokeLineJoin="bevel" + android:strokeWidth="1.75" + android:fillColor="#00000000" + android:strokeColor="#A0C2F9" + android:strokeLineCap="square"/> + <path + android:pathData="M420.9,325.8l-37.8,-88.6l-58.4,-37.8c-5.7,-5.4 -7.4,-13.8 -4.2,-21l0,0c2,-4.6 7.4,-6.7 12,-4.6c0.2,0.1 0.4,0.2 0.7,0.3c0,0 70.7,36.3 81.5,48.3l59.8,95.5l-49.9,24.9L420.9,325.8z" + android:fillColor="#D2E3FC"/> + <path + android:pathData="M324.6,183.9l8,6.2c2.1,1.7 5.2,1.4 7,-0.6l2.9,-3.3" + android:strokeLineJoin="bevel" + android:strokeWidth="1.75" + android:fillColor="#00000000" + android:strokeColor="#A0C2F9" + android:strokeLineCap="square"/> + <path + android:pathData="M392.4,231c3.8,-5.1 9.1,-8.9 15.1,-10.9" + android:strokeWidth="1.75" + android:fillColor="#00000000" + android:strokeColor="#A0C2F9"/> + <path + android:pathData="M401.3,283.8L405.8,292.6" + android:strokeWidth="1.75" + android:fillColor="#00000000" + android:strokeColor="#A0C2F9"/> + <path + android:pathData="M378.2,346.9l-34.7,-75.6l-60,-61.2c-6.3,-4.7 -9,-12.8 -6.7,-20.4l0,0c1.5,-4.8 6.5,-7.5 11.3,-6c0.2,0.1 0.4,0.1 0.7,0.2c0,0 73.5,48.2 82.6,61.7l64.1,95.7l-40.3,23.5L378.2,346.9z" + android:fillColor="#D2E3FC"/> + <path + android:pathData="M280.8,196.6l7.6,4.6c2.6,1.6 5.9,1.1 7.9,-1.1l4.1,-4.5" + android:strokeLineJoin="bevel" + android:strokeWidth="1.75" + android:fillColor="#00000000" + android:strokeColor="#A0C2F9" + android:strokeLineCap="square"/> + <path + android:pathData="M347.5,251c3.8,-5.1 9.1,-8.9 15.1,-10.9" + android:strokeWidth="1.75" + android:fillColor="#00000000" + android:strokeColor="#A0C2F9"/> + <path + android:pathData="M207.2,234.1c-8.8,-11 4.7,-31.5 19.8,-19c17.7,14.7 74.7,64.3 74.7,64.3l103.8,101.8c0,0 -36.4,53.8 -44.5,42.3C287.8,319.3 234.4,267.9 207.2,234.1z" + android:fillColor="#D2E3FC"/> + <path + android:pathData="M209.6,226.2l9.3,9.5c1,0.8 3,0.4 3.1,-1c0.2,-2.2 4.6,-6.2 7,-6.6c1.1,-0.3 1.7,-1.4 1.4,-2.4c-0.1,-0.2 -0.2,-0.4 -0.3,-0.6l-4.4,-3.9" + android:strokeLineJoin="bevel" + android:strokeWidth="1.75" + android:fillColor="#00000000" + android:strokeColor="#A0C2F9" + android:strokeLineCap="square"/> + <path + android:pathData="M284.1,296.2c3.1,-5.5 7.8,-10 13.5,-12.8" + android:strokeWidth="1.75" + android:fillColor="#00000000" + android:strokeColor="#A0C2F9"/> + </group> + <path + android:pathData="M206,4c80.6,0 146,65.4 146,146c0,38.7 -15.4,75.9 -42.8,103.2c-57,57 -149.5,57 -206.5,0s-57,-149.5 0,-206.5C130.1,19.3 167.3,3.9 206,4M206,0C123.2,0 56,67.2 56,150s67.2,150 150,150s150,-67.2 150,-150S288.8,0 206,0z" + android:fillColor="#D2E3FC"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/bubble_ic_empty_overflow_light.xml b/libs/WindowManager/Shell/res/drawable/bubble_ic_empty_overflow_light.xml new file mode 100644 index 000000000000..5e02f67700e7 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/bubble_ic_empty_overflow_light.xml @@ -0,0 +1,162 @@ +<!-- +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="412dp" + android:height="300dp" + android:viewportWidth="412" + android:viewportHeight="300"> + <group> + <clip-path + android:pathData="M206,150m-150,0a150,150 0,1 1,300 0a150,150 0,1 1,-300 0"/> + <path + android:pathData="M296,105.2h-9.6l-3.1,-2.5l-3.1,2.5H116c-1.7,0 -3,1.3 -3,3v111.7c0,1.7 1.3,3 3,3h180c1.7,0 3,-1.3 3,-3V108.2C299,106.6 297.7,105.2 296,105.2L296,105.2z" + android:fillColor="#F1F3F4"/> + <path + android:strokeWidth="1" + android:pathData="M252.4,85.3m-12.4,0a12.4,12.4 0,1 1,24.8 0a12.4,12.4 0,1 1,-24.8 0" + android:fillColor="#D2E3FC" + android:strokeColor="#4285F4"/> + <path + android:pathData="M261.9,95.7m-4.5,0a4.5,4.5 0,1 1,9 0a4.5,4.5 0,1 1,-9 0" + android:fillColor="#4285F4"/> + <path + android:strokeWidth="1" + android:pathData="M160.6,85.3m-12.4,0a12.4,12.4 0,1 1,24.8 0a12.4,12.4 0,1 1,-24.8 0" + android:fillColor="#FAD2CF" + android:strokeColor="#EA4335"/> + <path + android:pathData="M170.1,95.7m-4.6,0a4.6,4.6 0,1 1,9.2 0a4.6,4.6 0,1 1,-9.2 0" + android:fillColor="#EA4335"/> + <path + android:strokeWidth="1" + android:pathData="M192.1,85.3m-12.4,0a12.4,12.4 0,1 1,24.8 0a12.4,12.4 0,1 1,-24.8 0" + android:fillColor="#FEEFC3" + android:strokeColor="#FBBC04"/> + <path + android:strokeWidth="1" + android:pathData="M221.8,85.4m-12.4,0a12.4,12.4 0,1 1,24.8 0a12.4,12.4 0,1 1,-24.8 0" + android:fillColor="#CEEAD6" + android:strokeColor="#34A853"/> + <path + android:pathData="M201.6,95.7m-4.6,0a4.6,4.6 0,1 1,9.2 0a4.6,4.6 0,1 1,-9.2 0" + android:fillColor="#FBBC04"/> + <path + android:pathData="M231.4,95.7m-4.6,0a4.6,4.6 0,1 1,9.2 0a4.6,4.6 0,1 1,-9.2 0" + android:fillColor="#34A853"/> + <path + android:pathData="M282.8,85.3m-12.4,0a12.4,12.4 0,1 1,24.8 0a12.4,12.4 0,1 1,-24.8 0" + android:fillColor="#F1F3F4"/> + <path + android:pathData="M278.7,85.7m-1.2,0a1.2,1.2 0,1 1,2.4 0a1.2,1.2 0,1 1,-2.4 0" + android:fillColor="#3474E0"/> + <path + android:pathData="M282.8,85.7m-1.2,0a1.2,1.2 0,1 1,2.4 0a1.2,1.2 0,1 1,-2.4 0" + android:fillColor="#3474E0"/> + <path + android:pathData="M286.9,85.7m-1.2,0a1.2,1.2 0,1 1,2.4 0a1.2,1.2 0,1 1,-2.4 0" + android:fillColor="#3474E0"/> + <path + android:pathData="M129.2,72.9h-3.4l0.3,1c1,-0.3 2.1,-0.4 3.2,-0.4v-1v0.4H129.2zM122.6,74.8c-0.5,0.3 -1,0.6 -1.4,1l0,0l0,0l0,0l0,0h-0.6l0,0l0,0l0,0l0,0l0,0l0,0c-0.2,0.2 -0.3,0.3 -0.4,0.5L121,77c0.7,-0.8 1.5,-1.5 2.4,-2.1l-0.5,-0.8L122.6,74.8zM118,80L118,80L118,80L118,80L118,80L118,80L118,80L118,80c-0.5,1 -0.8,2 -1,3l1,0.2c0.2,-1 0.5,-2 1,-3L118,80zM117.8,86.7l-1,0.1c0.1,0.6 0.2,1.1 0.3,1.7l0,0l0,0h0.1l0,0l0,0l0,0l0,0c0.1,0.5 0.3,0.9 0.5,1.4l0.9,-0.4c-0.4,-1 -0.7,-2 -0.8,-3.1V86.7zM120.2,92.5l-0.8,0.6l0.2,0.3l0,0l0,0l0,0l0,0h0.3l0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0l0.4,0.4l0,0l0,0l0,0l0,0h0.1l0,0l0,0l0,0l0,0l0,0h0.5l0,0l0,0l0,0l0,0l0,0l0,0l0.6,0.4l0.6,-0.8c-0.9,-0.6 -1.7,-1.4 -2.3,-2.2L120.2,92.5zM125.4,96.2l-0.3,0.9c1.1,0.4 2.2,0.6 3.4,0.7l0.1,-1C127.5,96.7 126.4,96.5 125.4,96.2zM134.7,95.4c-0.9,0.5 -2,0.9 -3,1.1l0.2,1h0.4c1,-0.3 2,-0.6 2.9,-1.2L134.7,95.4L134.7,95.4zM139.2,90.9c-0.5,0.9 -1.2,1.8 -1.9,2.5l0.7,0.7V94h0.2l0,0l0,0c0.7,-0.7 1.3,-1.6 1.8,-2.4l-0.9,-0.5L139.2,90.9zM141.6,84.7h-1c0,0.2 0,0.4 0,0.6c0,0.9 -0.1,1.7 -0.3,2.6l1,0.2c0.1,-0.4 0.2,-0.8 0.2,-1.2l0,0v-0.1l0,0v-0.1l0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0l0,0c0,-0.2 0,-0.3 0,-0.5l0,0L141.6,84.7zM139.3,78.2l-0.8,0.6c0.6,0.9 1.1,1.8 1.5,2.8l0.9,-0.3c-0.1,-0.2 -0.2,-0.4 -0.2,-0.7l0,0l0,0h-0.1l0,0l0,0l0,0l0,0l0,0l0,0c-0.3,-0.7 -0.7,-1.4 -1.1,-2l0,0l0,0l0,0l0,0l0,0l0,0l0,0L139.3,78.2zM134,73.9l-0.4,0.9c1,0.4 1.9,1 2.7,1.6l0.6,-0.8l0,0l0,0l0,0l0,0l0,0c-0.3,-0.3 -0.7,-0.5 -1,-0.7l0,0h-0.1h-0.6c-0.4,-0.2 -0.8,-0.4 -1.2,-0.6V73.9zM129.2,72.9v1c0.4,0 0.9,0 1.3,0.1l0.1,-1l-0.9,-0.1H129.2L129.2,72.9z" + android:fillColor="#34A853"/> + <path + android:pathData="M206,252m-11.7,0a11.7,11.7 0,1 1,23.4 0a11.7,11.7 0,1 1,-23.4 0" + android:fillColor="#9AA0A6"/> + <path + android:pathData="M201.7,247.7L210.3,256.3" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#F1F3F4" + android:strokeLineCap="round"/> + <path + android:pathData="M210.3,247.7L201.7,256.3" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#F1F3F4" + android:strokeLineCap="round"/> + <path + android:strokeWidth="1" + android:pathData="M205.3,221.9m-10.4,0a10.4,10.4 0,1 1,20.8 0a10.4,10.4 0,1 1,-20.8 0" + android:fillColor="#CEEAD6" + android:strokeColor="#34A853"/> + <path + android:pathData="M481.4,292.2c48,58.3 119.8,125.8 58.6,162.9c-38.7,23.5 -53.9,24 -98.3,33.2c-43.8,9.1 -93.6,-89.8 -101.1,-134.5C329.6,288.6 452.6,257.2 481.4,292.2z" + android:fillColor="#D2E3FC"/> + <path + android:pathData="M458.2,320.7l-21.1,-71.4L400.5,193c-2.7,-5.1 -1.2,-11.4 3.5,-14.7l0,0c2.8,-2 6.6,-1.5 8.8,1.1c0,0 40.6,38.4 53.2,61.1l81.5,134.8l-69.9,-39.1L458.2,320.7z" + android:fillColor="#D2E3FC"/> + <path + android:pathData="M403.8,184.8l5.4,6.9c1.2,1.5 3.3,1.9 4.9,0.9l3,-1.8" + android:strokeLineJoin="bevel" + android:strokeWidth="1.75" + android:fillColor="#00000000" + android:strokeColor="#A0C2F9" + android:strokeLineCap="square"/> + <path + android:pathData="M420.9,325.8l-37.8,-88.6l-58.4,-37.8c-5.7,-5.4 -7.4,-13.8 -4.2,-21l0,0c2,-4.6 7.4,-6.7 12,-4.6c0.2,0.1 0.4,0.2 0.7,0.3c0,0 70.7,36.3 81.5,48.3l59.8,95.5l-49.9,24.9L420.9,325.8z" + android:fillColor="#D2E3FC"/> + <path + android:pathData="M324.6,183.9l8,6.2c2.1,1.7 5.2,1.4 7,-0.6l2.9,-3.3" + android:strokeLineJoin="bevel" + android:strokeWidth="1.75" + android:fillColor="#00000000" + android:strokeColor="#A0C2F9" + android:strokeLineCap="square"/> + <path + android:pathData="M392.4,231c3.8,-5.1 9.1,-8.9 15.1,-10.9" + android:strokeWidth="1.75" + android:fillColor="#00000000" + android:strokeColor="#A0C2F9"/> + <path + android:pathData="M401.3,283.8L405.8,292.6" + android:strokeWidth="1.75" + android:fillColor="#00000000" + android:strokeColor="#A0C2F9"/> + <path + android:pathData="M378.2,346.9l-34.7,-75.6l-60,-61.2c-6.3,-4.7 -9,-12.8 -6.7,-20.4l0,0c1.5,-4.8 6.5,-7.5 11.3,-6c0.2,0.1 0.4,0.1 0.7,0.2c0,0 73.5,48.2 82.6,61.7l64.1,95.7l-40.3,23.5L378.2,346.9z" + android:fillColor="#D2E3FC"/> + <path + android:pathData="M280.8,196.6l7.6,4.6c2.6,1.6 5.9,1.1 7.9,-1.1l4.1,-4.5" + android:strokeLineJoin="bevel" + android:strokeWidth="1.75" + android:fillColor="#00000000" + android:strokeColor="#A0C2F9" + android:strokeLineCap="square"/> + <path + android:pathData="M347.5,251c3.8,-5.1 9.1,-8.9 15.1,-10.9" + android:strokeWidth="1.75" + android:fillColor="#00000000" + android:strokeColor="#A0C2F9"/> + <path + android:pathData="M207.2,234.1c-8.8,-11 4.7,-31.5 19.8,-19c17.7,14.7 74.7,64.3 74.7,64.3l103.8,101.8c0,0 -36.4,53.8 -44.5,42.3C287.8,319.3 234.4,267.9 207.2,234.1z" + android:fillColor="#D2E3FC"/> + <path + android:pathData="M209.6,226.2l9.3,9.5c1,0.8 3,0.4 3.1,-1c0.2,-2.2 4.6,-6.2 7,-6.6c1.1,-0.3 1.7,-1.4 1.4,-2.4c-0.1,-0.2 -0.2,-0.4 -0.3,-0.6l-4.4,-3.9" + android:strokeLineJoin="bevel" + android:strokeWidth="1.75" + android:fillColor="#00000000" + android:strokeColor="#A0C2F9" + android:strokeLineCap="square"/> + <path + android:pathData="M284.1,296.2c3.1,-5.5 7.8,-10 13.5,-12.8" + android:strokeWidth="1.75" + android:fillColor="#00000000" + android:strokeColor="#A0C2F9"/> + </group> + <path + android:pathData="M206,4c80.6,0 146,65.4 146,146c0,38.7 -15.4,75.9 -42.8,103.2c-57,57 -149.5,57 -206.5,0s-57,-149.5 0,-206.5C130.1,19.3 167.3,3.9 206,4M206,0C123.2,0 56,67.2 56,150s67.2,150 150,150s150,-67.2 150,-150S288.8,0 206,0z" + android:fillColor="#D2E3FC"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/bubble_ic_overflow_button.xml b/libs/WindowManager/Shell/res/drawable/bubble_ic_overflow_button.xml new file mode 100644 index 000000000000..3acebc12a807 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/bubble_ic_overflow_button.xml @@ -0,0 +1,24 @@ +<!-- +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:viewportWidth="24" + android:viewportHeight="24" + android:width="24dp" + android:height="24dp"> + <path + android:fillColor="#1A73E8" + android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/bubble_ic_stop_bubble.xml b/libs/WindowManager/Shell/res/drawable/bubble_ic_stop_bubble.xml new file mode 100644 index 000000000000..8609576ce789 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/bubble_ic_stop_bubble.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="20dp" + android:height="20dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="#FF000000" + android:pathData="M11.29,14.71L7,10.41V13H5V7h6v2H8.41l4.29,4.29L11.29,14.71zM21,3H3C1.9,3 1,3.9 1,5v14c0,1.1 0.9,2 2,2h10v0v-2H3V5h18v8h2V5C23,3.9 22.1,3 21,3zM19,15c-1.66,0 -3,1.34 -3,3s1.34,3 3,3s3,-1.34 3,-3S20.66,15 19,15z"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/bubble_manage_menu_row.xml b/libs/WindowManager/Shell/res/drawable/bubble_manage_menu_row.xml new file mode 100644 index 000000000000..c61ac1c5f2d5 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/bubble_manage_menu_row.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. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_pressed="true"> + <ripple android:color="#99999999" /> + </item> +</selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/bubble_stack_user_education_bg.xml b/libs/WindowManager/Shell/res/drawable/bubble_stack_user_education_bg.xml new file mode 100644 index 000000000000..4b9219cd6194 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/bubble_stack_user_education_bg.xml @@ -0,0 +1,22 @@ +<!-- + ~ 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"> + <solid android:color="?android:attr/colorAccent"/> + <corners + android:bottomRightRadius="360dp" + android:topRightRadius="360dp" /> +</shape>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/bubble_stack_user_education_bg_rtl.xml b/libs/WindowManager/Shell/res/drawable/bubble_stack_user_education_bg_rtl.xml new file mode 100644 index 000000000000..c7baba14b5e5 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/bubble_stack_user_education_bg_rtl.xml @@ -0,0 +1,22 @@ +<!-- + ~ 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"> + <solid android:color="?android:attr/colorAccent"/> + <corners + android:bottomLeftRadius="360dp" + android:topLeftRadius="360dp" /> +</shape> 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/ic_remove_no_shadow.xml b/libs/WindowManager/Shell/res/drawable/ic_remove_no_shadow.xml new file mode 100644 index 000000000000..265c5019c79a --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/ic_remove_no_shadow.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" + android:tint="?android:attr/textColorPrimary" > + <path + android:fillColor="#FFFFFFFF" + android:pathData="M13.41,12l5.29-5.29c0.39-0.39,0.39-1.02,0-1.41c-0.39-0.39-1.02-0.39-1.41,0L12,10.59L6.71, + 5.29c-0.39-0.39-1.02-0.39-1.41,0c-0.39,0.39-0.39,1.02,0,1.41L10.59,12l-5.29,5.29c-0.39,0.39-0.39,1.02, + 0,1.41c0.39,0.39,1.02,0.39,1.41,0L12,13.41l5.29,5.29c0.39,0.39,1.02,0.39,1.41,0c0.39-0.39,0.39-1.02,0-1.41L13.41,12z"/> +</vector>
\ 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/rounded_bg_full.xml b/libs/WindowManager/Shell/res/drawable/rounded_bg_full.xml new file mode 100644 index 000000000000..e95744573960 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/rounded_bg_full.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. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="?android:attr/colorBackgroundFloating" /> + <corners + android:bottomLeftRadius="?android:attr/dialogCornerRadius" + android:topLeftRadius="?android:attr/dialogCornerRadius" + android:bottomRightRadius="?android:attr/dialogCornerRadius" + android:topRightRadius="?android:attr/dialogCornerRadius" + /> +</shape> diff --git a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml new file mode 100644 index 000000000000..73a48d31a814 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. +--> +<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="#aa000000" + android:pathData="M0,12 a12,12 0 1,0 24,0 a12,12 0 1,0 -24,0" /> + <path + android:fillColor="@android:color/white" + android:pathData="M17.65,6.35c-1.63,-1.63 -3.94,-2.57 -6.48,-2.31c-3.67,0.37 -6.69,3.35 -7.1,7.02C3.52,15.91 7.27,20 12,20c3.19,0 5.93,-1.87 7.21,-4.57c0.31,-0.66 -0.16,-1.43 -0.89,-1.43h-0.01c-0.37,0 -0.72,0.2 -0.88,0.53c-1.13,2.43 -3.84,3.97 -6.81,3.32c-2.22,-0.49 -4.01,-2.3 -4.49,-4.52C5.31,9.44 8.26,6 12,6c1.66,0 3.14,0.69 4.22,1.78l-2.37,2.37C13.54,10.46 13.76,11 14.21,11H19c0.55,0 1,-0.45 1,-1V5.21c0,-0.45 -0.54,-0.67 -0.85,-0.35L17.65,6.35z"/> +</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/bubble_dismiss_target.xml b/libs/WindowManager/Shell/res/layout/bubble_dismiss_target.xml new file mode 100644 index 000000000000..f5cd727a6d03 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/bubble_dismiss_target.xml @@ -0,0 +1,49 @@ +<!-- + ~ 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 + --> +<!-- Bubble dismiss target consisting of an X icon and the text 'Dismiss'. --> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="@dimen/floating_dismiss_gradient_height" + android:layout_gravity="bottom|center_horizontal"> + + <FrameLayout + android:id="@+id/bubble_dismiss_circle" + android:layout_width="@dimen/bubble_dismiss_encircle_size" + android:layout_height="@dimen/bubble_dismiss_encircle_size" + android:layout_gravity="center" + android:background="@drawable/bubble_dismiss_circle" /> + + <LinearLayout + android:id="@+id/bubble_dismiss_icon_container" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center" + android:paddingBottom="@dimen/bubble_dismiss_target_padding_y" + android:paddingTop="@dimen/bubble_dismiss_target_padding_y" + android:paddingLeft="@dimen/bubble_dismiss_target_padding_x" + android:paddingRight="@dimen/bubble_dismiss_target_padding_x" + android:clipChildren="false" + android:clipToPadding="false" + android:orientation="horizontal"> + + <ImageView + android:id="@+id/bubble_dismiss_close_icon" + android:layout_width="24dp" + android:layout_height="24dp" + android:src="@drawable/bubble_dismiss_icon" /> + </LinearLayout> +</FrameLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/bubble_expanded_view.xml b/libs/WindowManager/Shell/res/layout/bubble_expanded_view.xml new file mode 100644 index 000000000000..81656fe7e80d --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/bubble_expanded_view.xml @@ -0,0 +1,41 @@ +<?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.bubbles.BubbleExpandedView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:orientation="vertical" + android:id="@+id/bubble_expanded_view"> + + <View + android:id="@+id/pointer_view" + android:layout_width="@dimen/bubble_pointer_width" + android:layout_height="@dimen/bubble_pointer_height" + /> + + <com.android.wm.shell.common.AlphaOptimizedButton + style="@android:style/Widget.Material.Button.Borderless" + android:id="@+id/settings_button" + android:layout_gravity="start" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:focusable="true" + android:text="@string/manage_bubbles_text" + android:textColor="?android:attr/textColorPrimary" + /> + +</com.android.wm.shell.bubbles.BubbleExpandedView> diff --git a/libs/WindowManager/Shell/res/layout/bubble_flyout.xml b/libs/WindowManager/Shell/res/layout/bubble_flyout.xml new file mode 100644 index 000000000000..7fdf290efd09 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/bubble_flyout.xml @@ -0,0 +1,66 @@ +<!-- + ~ 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 + --> +<merge xmlns:android="http://schemas.android.com/apk/res/android"> + + <LinearLayout + android:id="@+id/bubble_flyout_text_container" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:orientation="horizontal" + android:clipToPadding="false" + android:clipChildren="false" + android:paddingStart="@dimen/bubble_flyout_padding_x" + android:paddingEnd="@dimen/bubble_flyout_padding_x" + android:paddingTop="@dimen/bubble_flyout_padding_y" + android:paddingBottom="@dimen/bubble_flyout_padding_y" + android:translationZ="@dimen/bubble_flyout_elevation"> + + <ImageView + android:id="@+id/bubble_flyout_avatar" + android:layout_width="30dp" + android:layout_height="30dp" + android:layout_marginEnd="@dimen/bubble_flyout_avatar_message_space" + android:scaleType="centerInside" + android:src="@drawable/bubble_ic_create_bubble"/> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <TextView + android:id="@+id/bubble_flyout_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="@*android:string/config_bodyFontFamilyMedium" + android:maxLines="1" + android:ellipsize="end" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body2"/> + + <TextView + android:id="@+id/bubble_flyout_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="@*android:string/config_bodyFontFamily" + android:maxLines="2" + android:ellipsize="end" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body2"/> + + </LinearLayout> + + </LinearLayout> + +</merge>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml b/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml new file mode 100644 index 000000000000..3a6aa805d071 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml @@ -0,0 +1,99 @@ +<?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="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/rounded_bg_full" + android:elevation="@dimen/bubble_manage_menu_elevation" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/bubble_manage_menu_dismiss_container" + android:background="@drawable/bubble_manage_menu_row" + android:layout_width="match_parent" + android:layout_height="48dp" + android:gravity="center_vertical" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:orientation="horizontal"> + + <ImageView + android:layout_width="24dp" + android:layout_height="24dp" + android:src="@drawable/ic_remove_no_shadow" + android:tint="@color/bubbles_icon_tint"/> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault" + android:text="@string/bubble_dismiss_text" /> + + </LinearLayout> + + <LinearLayout + android:id="@+id/bubble_manage_menu_dont_bubble_container" + android:background="@drawable/bubble_manage_menu_row" + android:layout_width="match_parent" + android:layout_height="48dp" + android:gravity="center_vertical" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:orientation="horizontal"> + + <ImageView + android:layout_width="24dp" + android:layout_height="24dp" + android:src="@drawable/bubble_ic_stop_bubble" + android:tint="@color/bubbles_icon_tint"/> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault" + android:text="@string/bubbles_dont_bubble_conversation" /> + + </LinearLayout> + + <LinearLayout + android:id="@+id/bubble_manage_menu_settings_container" + android:background="@drawable/bubble_manage_menu_row" + android:layout_width="match_parent" + android:layout_height="48dp" + android:gravity="center_vertical" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:orientation="horizontal"> + + <ImageView + android:id="@+id/bubble_manage_menu_settings_icon" + android:layout_width="24dp" + android:layout_height="24dp" + android:src="@drawable/ic_remove_no_shadow"/> + + <TextView + android:id="@+id/bubble_manage_menu_settings_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault" /> + + </LinearLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/bubble_menu_view.xml b/libs/WindowManager/Shell/res/layout/bubble_menu_view.xml new file mode 100644 index 000000000000..d19b65394cd8 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/bubble_menu_view.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<com.android.wm.shell.bubbles.BubbleMenuView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_height="match_parent" + android:layout_width="match_parent" + android:background="#66000000" + android:visibility="gone" + android:id="@+id/bubble_menu_container" + tools:ignore="MissingClass"> + + <FrameLayout + android:layout_height="@dimen/bubble_menu_item_height" + android:layout_width="wrap_content" + android:background="#FFFFFF" + android:id="@+id/bubble_menu_view"> + + <ImageView + android:id="@*android:id/icon" + android:layout_width="@dimen/bubble_grid_item_icon_width" + android:layout_height="@dimen/bubble_grid_item_icon_height" + android:layout_marginTop="@dimen/bubble_grid_item_icon_top_margin" + android:layout_marginBottom="@dimen/bubble_grid_item_icon_bottom_margin" + android:layout_marginLeft="@dimen/bubble_grid_item_icon_side_margin" + android:layout_marginRight="@dimen/bubble_grid_item_icon_side_margin" + android:scaleType="centerInside" + android:tint="@color/bubbles_icon_tint" + /> + </FrameLayout> +</com.android.wm.shell.bubbles.BubbleMenuView> diff --git a/libs/WindowManager/Shell/res/layout/bubble_overflow_button.xml b/libs/WindowManager/Shell/res/layout/bubble_overflow_button.xml new file mode 100644 index 000000000000..e392cdc26c60 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/bubble_overflow_button.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 + --> +<com.android.wm.shell.bubbles.BadgedImageView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/bubble_overflow_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/bubble_ic_overflow_button"/> diff --git a/libs/WindowManager/Shell/res/layout/bubble_overflow_container.xml b/libs/WindowManager/Shell/res/layout/bubble_overflow_container.xml new file mode 100644 index 000000000000..8224d95fd9ad --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/bubble_overflow_container.xml @@ -0,0 +1,72 @@ +<!-- + ~ 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 + --> + +<com.android.wm.shell.bubbles.BubbleOverflowContainerView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/bubble_overflow_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingTop="@dimen/bubble_overflow_padding" + android:paddingLeft="@dimen/bubble_overflow_padding" + android:paddingRight="@dimen/bubble_overflow_padding" + android:orientation="vertical" + android:layout_gravity="center_horizontal"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/bubble_overflow_recycler" + android:layout_gravity="center_horizontal" + android:nestedScrollingEnabled="false" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + + <LinearLayout + android:id="@+id/bubble_overflow_empty_state" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingLeft="@dimen/bubble_overflow_empty_state_padding" + android:paddingRight="@dimen/bubble_overflow_empty_state_padding" + android:orientation="vertical" + android:gravity="center"> + + <ImageView + android:layout_width="@dimen/bubble_empty_overflow_image_height" + android:layout_height="@dimen/bubble_empty_overflow_image_height" + android:id="@+id/bubble_overflow_empty_state_image" + android:scaleType="fitCenter" + android:layout_gravity="center"/> + + <TextView + android:id="@+id/bubble_overflow_empty_title" + android:text="@string/bubble_overflow_empty_title" + android:fontFamily="@*android:string/config_bodyFontFamilyMedium" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body2" + android:textColor="?android:attr/textColorSecondary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center"/> + + <TextView + android:id="@+id/bubble_overflow_empty_subtitle" + android:fontFamily="@*android:string/config_bodyFontFamily" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body2" + android:textColor="?android:attr/textColorSecondary" + android:text="@string/bubble_overflow_empty_subtitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingBottom="@dimen/bubble_empty_overflow_subtitle_padding" + android:gravity="center"/> + </LinearLayout> +</com.android.wm.shell.bubbles.BubbleOverflowContainerView> diff --git a/libs/WindowManager/Shell/res/layout/bubble_overflow_view.xml b/libs/WindowManager/Shell/res/layout/bubble_overflow_view.xml new file mode 100644 index 000000000000..c1f67bd27d93 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/bubble_overflow_view.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 + --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/bubble_overflow_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <com.android.wm.shell.bubbles.BadgedImageView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/bubble_view" + android:layout_gravity="center" + android:layout_width="@dimen/individual_bubble_size" + android:layout_height="@dimen/individual_bubble_size"/> + + <TextView + android:id="@+id/bubble_view_name" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textSize="13sp" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:maxLines="1" + android:lines="2" + android:ellipsize="end" + android:layout_gravity="center" + android:paddingTop="@dimen/bubble_overflow_text_padding" + android:gravity="center"/> +</LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml b/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml new file mode 100644 index 000000000000..fe1ed4b6f726 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml @@ -0,0 +1,53 @@ +<?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 + android:id="@+id/stack_education_layout" + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:paddingTop="48dp" + android:paddingBottom="48dp" + android:paddingStart="@dimen/bubble_stack_user_education_side_inset" + android:paddingEnd="16dp" + android:layout_marginEnd="24dp" + android:orientation="vertical" + android:background="@drawable/bubble_stack_user_education_bg" + > + <TextView + android:id="@+id/stack_education_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingBottom="16dp" + android:fontFamily="@*android:string/config_bodyFontFamilyMedium" + android:maxLines="2" + android:ellipsize="end" + android:gravity="start" + android:textAlignment="viewStart" + android:text="@string/bubbles_user_education_title" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Headline"/> + + <TextView + android:id="@+id/stack_education_description" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ellipsize="end" + android:gravity="start" + android:textAlignment="viewStart" + android:text="@string/bubbles_user_education_description" + android:fontFamily="@*android:string/config_bodyFontFamily" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body2"/> +</LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/bubble_view.xml b/libs/WindowManager/Shell/res/layout/bubble_view.xml new file mode 100644 index 000000000000..2b4b9e9042c9 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/bubble_view.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<com.android.wm.shell.bubbles.BadgedImageView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/bubble_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> diff --git a/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml b/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml new file mode 100644 index 000000000000..8de06c7732d4 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml @@ -0,0 +1,90 @@ +<?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/manage_education_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:clickable="true" + android:paddingTop="28dp" + android:paddingBottom="16dp" + android:paddingStart="@dimen/bubble_expanded_view_padding" + android:paddingEnd="48dp" + android:layout_marginEnd="24dp" + android:orientation="vertical" + android:background="@drawable/bubble_stack_user_education_bg" + > + + <TextView + android:id="@+id/user_education_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingStart="16dp" + android:paddingBottom="16dp" + android:fontFamily="@*android:string/config_bodyFontFamilyMedium" + android:maxLines="2" + android:ellipsize="end" + android:gravity="start" + android:textAlignment="viewStart" + android:text="@string/bubbles_user_education_manage_title" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Headline"/> + + <TextView + android:id="@+id/user_education_description" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingStart="16dp" + android:paddingBottom="24dp" + android:text="@string/bubbles_user_education_manage" + android:maxLines="2" + android:ellipsize="end" + android:gravity="start" + android:textAlignment="viewStart" + android:fontFamily="@*android:string/config_bodyFontFamily" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body2"/> + + <LinearLayout + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:id="@+id/button_layout" + android:orientation="horizontal" > + + <com.android.wm.shell.common.AlphaOptimizedButton + style="@android:style/Widget.Material.Button.Borderless" + android:id="@+id/manage" + android:layout_gravity="start" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:focusable="true" + android:clickable="false" + android:text="@string/manage_bubbles_text" + android:textColor="?android:attr/textColorPrimaryInverse" + /> + + <com.android.wm.shell.common.AlphaOptimizedButton + style="@android:style/Widget.Material.Button.Borderless" + android:id="@+id/got_it" + android:layout_gravity="start" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:focusable="true" + android:text="@string/bubbles_user_education_got_it" + android:textColor="?android:attr/textColorPrimaryInverse" + /> + </LinearLayout> +</LinearLayout> 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..ed5d2e1b49f5 --- /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.legacysplitscreen.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.legacysplitscreen.MinimizedDockShadow + style="@style/DockedDividerMinimizedShadow" + android:id="@+id/minimized_dock_shadow" + android:alpha="0"/> + + <com.android.wm.shell.common.split.DividerHandleView + style="@style/DockedDividerHandle" + android:id="@+id/docked_divider_handle" + android:contentDescription="@string/accessibility_divider" + android:background="@null"/> + +</com.android.wm.shell.legacysplitscreen.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/global_drop_target.xml b/libs/WindowManager/Shell/res/layout/global_drop_target.xml new file mode 100644 index 000000000000..0dd0b0048382 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/global_drop_target.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. +--> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" /> 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..93a6e7b70c54 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/pip_menu.xml @@ -0,0 +1,110 @@ +<?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="@dimen/pip_expand_action_size" + android:layout_height="@dimen/pip_expand_action_size" + 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" /> + + <!-- invisible layer to trap the focus, b/169372603 --> + <FrameLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@null" + android:defaultFocusHighlightEnabled="false" + android:focusable="true" + android:focusableInTouchMode="true" + android:focusedByDefault="true" /> +t </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/size_compat_mode_hint.xml b/libs/WindowManager/Shell/res/layout/size_compat_mode_hint.xml new file mode 100644 index 000000000000..347c2b47767e --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/size_compat_mode_hint.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@android:color/background_light" + android:orientation="vertical"> + + <TextView + android:layout_width="180dp" + android:layout_height="wrap_content" + android:paddingLeft="10dp" + android:paddingRight="10dp" + android:paddingTop="10dp" + android:text="@string/restart_button_description" + android:textAlignment="viewStart" + android:textColor="@android:color/primary_text_light" + android:textSize="16sp" /> + + <Button + android:id="@+id/got_it" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:includeFontPadding="false" + android:layout_gravity="end" + android:minHeight="36dp" + android:background="?android:attr/selectableItemBackground" + android:text="@string/got_it" + android:textAllCaps="true" + android:textColor="#3c78d8" + android:textSize="16sp" + android:textStyle="bold" /> + +</LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/split_divider.xml b/libs/WindowManager/Shell/res/layout/split_divider.xml new file mode 100644 index 000000000000..7f583f3e6bac --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/split_divider.xml @@ -0,0 +1,33 @@ +<!-- + ~ 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.common.split.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.common.split.DividerHandleView + style="@style/DockedDividerHandle" + android:id="@+id/docked_divider_handle" + android:contentDescription="@string/accessibility_divider" + android:background="@null"/> + +</com.android.wm.shell.common.split.DividerView> 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..49e2379589a4 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml @@ -0,0 +1,52 @@ +<?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 TvPipMenuView --> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/tv_pip_menu" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="#CC000000"> + + <LinearLayout + android:id="@+id/tv_pip_menu_action_buttons" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:layout_marginTop="350dp" + android:orientation="horizontal" + android:alpha="0"> + + <com.android.wm.shell.pip.tv.TvPipMenuActionButton + android:id="@+id/tv_pip_menu_fullscreen_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.TvPipMenuActionButton + android:id="@+id/tv_pip_menu_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" /> + + <!-- More TvPipMenuActionButtons may be added here at runtime. --> + + </LinearLayout> + +</FrameLayout> diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu_action_button.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu_action_button.xml new file mode 100644 index 000000000000..5925008e0d08 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu_action_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 TvPipMenuActionButton --> +<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_menu_additional_action_button.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu_additional_action_button.xml new file mode 100644 index 000000000000..bf4eb2691ff0 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu_additional_action_button.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.TvPipMenuActionButton + 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/raw/wm_shell_protolog.json b/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json new file mode 100644 index 000000000000..2cfb13e7dea6 --- /dev/null +++ b/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json @@ -0,0 +1,232 @@ +{ + "version": "1.0.0", + "messages": { + "-2076257741": { + "message": "Transition requested: %s %s", + "level": "VERBOSE", + "group": "WM_SHELL_TRANSITIONS", + "at": "com\/android\/wm\/shell\/transition\/Transitions.java" + }, + "-1683614271": { + "message": "Existing task: id=%d component=%s", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" + }, + "-1501874464": { + "message": "Fullscreen Task Appeared: #%d", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/FullscreenTaskListener.java" + }, + "-1382704050": { + "message": "Display removed: %d", + "level": "VERBOSE", + "group": "WM_SHELL_DRAG_AND_DROP", + "at": "com\/android\/wm\/shell\/draganddrop\/DragAndDropController.java" + }, + "-1362429294": { + "message": "%s onTaskAppeared Primary taskId=%d", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/legacysplitscreen\/LegacySplitScreenTaskListener.java" + }, + "-1340279385": { + "message": "Remove listener=%s", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" + }, + "-1325223370": { + "message": "Task appeared taskId=%d listener=%s", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" + }, + "-1312360667": { + "message": "createRootTask() displayId=%d winMode=%d listener=%s", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" + }, + "-1006733970": { + "message": "Display added: %d", + "level": "VERBOSE", + "group": "WM_SHELL_DRAG_AND_DROP", + "at": "com\/android\/wm\/shell\/draganddrop\/DragAndDropController.java" + }, + "-1000962629": { + "message": "Animate bounds: from=%s to=%s", + "level": "VERBOSE", + "group": "WM_SHELL_DRAG_AND_DROP", + "at": "com\/android\/wm\/shell\/draganddrop\/DropOutlineDrawable.java" + }, + "-880817403": { + "message": "Task vanished taskId=%d", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" + }, + "-742394458": { + "message": "pair task1=%d task2=%d in AppPair=%s", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/apppairs\/AppPair.java" + }, + "-710770147": { + "message": "Add target: %s", + "level": "VERBOSE", + "group": "WM_SHELL_DRAG_AND_DROP", + "at": "com\/android\/wm\/shell\/draganddrop\/DragLayout.java" + }, + "-298656957": { + "message": "%s onTaskAppeared unknown taskId=%d winMode=%d", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/legacysplitscreen\/LegacySplitScreenTaskListener.java" + }, + "-234284913": { + "message": "unpair taskId=%d pair=%s", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/apppairs\/AppPairsController.java" + }, + "157713005": { + "message": "Task info changed taskId=%d", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" + }, + "274140888": { + "message": "Animate alpha: from=%d to=%d", + "level": "VERBOSE", + "group": "WM_SHELL_DRAG_AND_DROP", + "at": "com\/android\/wm\/shell\/draganddrop\/DropOutlineDrawable.java" + }, + "325110414": { + "message": "Transition animations finished, notifying core %s", + "level": "VERBOSE", + "group": "WM_SHELL_TRANSITIONS", + "at": "com\/android\/wm\/shell\/transition\/Transitions.java" + }, + "375908576": { + "message": "Clip description: handlingDrag=%b itemCount=%d mimeTypes=%s", + "level": "VERBOSE", + "group": "WM_SHELL_DRAG_AND_DROP", + "at": "com\/android\/wm\/shell\/draganddrop\/DragAndDropController.java" + }, + "473543554": { + "message": "%s onTaskAppeared Supported", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/legacysplitscreen\/LegacySplitScreenTaskListener.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" + }, + "900599280": { + "message": "Can't pair unresizeable tasks task1.isResizeable=%b task1.isResizeable=%b", + "level": "ERROR", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/apppairs\/AppPair.java" + }, + "950299522": { + "message": "taskId %d isn't isn't in an app-pair.", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/apppairs\/AppPairsController.java" + }, + "980952660": { + "message": "Task root back pressed taskId=%d", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" + }, + "982027396": { + "message": "%s onTaskAppeared Secondary taskId=%d", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/legacysplitscreen\/LegacySplitScreenTaskListener.java" + }, + "1070270131": { + "message": "onTransitionReady %s: %s", + "level": "VERBOSE", + "group": "WM_SHELL_TRANSITIONS", + "at": "com\/android\/wm\/shell\/transition\/Transitions.java" + }, + "1079041527": { + "message": "incrementPool size=%d", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/apppairs\/AppPairsPool.java" + }, + "1184615936": { + "message": "Set drop target window visibility: displayId=%d visibility=%d", + "level": "VERBOSE", + "group": "WM_SHELL_DRAG_AND_DROP", + "at": "com\/android\/wm\/shell\/draganddrop\/DragAndDropController.java" + }, + "1481772149": { + "message": "Current target: %s", + "level": "VERBOSE", + "group": "WM_SHELL_DRAG_AND_DROP", + "at": "com\/android\/wm\/shell\/draganddrop\/DragLayout.java" + }, + "1862198614": { + "message": "Drag event: action=%s x=%f y=%f xOffset=%f yOffset=%f", + "level": "VERBOSE", + "group": "WM_SHELL_DRAG_AND_DROP", + "at": "com\/android\/wm\/shell\/draganddrop\/DragAndDropController.java" + }, + "1891981945": { + "message": "release entry.taskId=%s listener=%s size=%d", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/apppairs\/AppPairsPool.java" + }, + "1990759023": { + "message": "addListenerForType types=%s listener=%s", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" + }, + "2006473416": { + "message": "acquire entry.taskId=%s listener=%s size=%d", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/apppairs\/AppPairsPool.java" + }, + "2057038970": { + "message": "Display changed: %d", + "level": "VERBOSE", + "group": "WM_SHELL_DRAG_AND_DROP", + "at": "com\/android\/wm\/shell\/draganddrop\/DragAndDropController.java" + } + }, + "groups": { + "WM_SHELL_DRAG_AND_DROP": { + "tag": "WindowManagerShell" + }, + "WM_SHELL_TASK_ORG": { + "tag": "WindowManagerShell" + }, + "WM_SHELL_TRANSITIONS": { + "tag": "WindowManagerShell" + } + } +} diff --git a/libs/WindowManager/Shell/res/values-af/strings.xml b/libs/WindowManager/Shell/res/values-af/strings.xml new file mode 100644 index 000000000000..ea634cfa907c --- /dev/null +++ b/libs/WindowManager/Shell/res/values-af/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Maak toe"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Vou uit"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Instellings"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Kieslys"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in beeld-in-beeld"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"As jy nie wil hê dat <xliff:g id="NAME">%s</xliff:g> hierdie kenmerk moet gebruik nie, tik om instellings oop te maak en skakel dit af."</string> + <string name="pip_play" msgid="3496151081459417097">"Speel"</string> + <string name="pip_pause" msgid="690688849510295232">"Laat wag"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Slaan oor na volgende"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Slaan oor na vorige"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Verander grootte"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Program sal dalk nie met verdeelde skerm werk nie."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Program steun nie verdeelde skerm nie."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Program sal dalk nie op \'n sekondêre skerm werk nie."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Program steun nie begin op sekondêre skerms nie."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Skermverdeler"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Volskerm links"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Links 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Links 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Links 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Volskerm regs"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Volskerm bo"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Bo 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Bo 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Bo 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Volskerm onder"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Gebruik eenhandmodus"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Swiep van die onderkant van die skerm af op of tik enige plek bo die program om uit te gaan"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Begin eenhandmodus"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Verlaat eenhandmodus"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Instellings vir <xliff:g id="APP_NAME">%1$s</xliff:g>-borrels"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Oorloop"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Voeg terug op stapel"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> vanaf <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> van <xliff:g id="APP_NAME">%2$s</xliff:g> en <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> meer af"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Beweeg na links bo"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Beweeg na regs bo"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Beweeg na links onder"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Beweeg na regs onder"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>-instellings"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Maak borrel toe"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Moenie dat gesprek \'n borrel word nie"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Klets met borrels"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nuwe gesprekke verskyn as swerwende ikone, of borrels Tik op borrel om dit oop te maak. Sleep om dit te skuif."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Beheer borrels enige tyd"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Tik op Bestuur om borrels vanaf hierdie program af te skakel"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Het dit"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Geen onlangse borrels nie"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Onlangse borrels en borrels wat toegemaak is, sal hier verskyn"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Borrel"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Bestuur"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Borrel is toegemaak."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-af/strings_tv.xml b/libs/WindowManager/Shell/res/values-af/strings_tv.xml new file mode 100644 index 000000000000..6ce588034f9e --- /dev/null +++ b/libs/WindowManager/Shell/res/values-af/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Beeld-in-beeld"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Titellose program)"</string> + <string name="pip_close" msgid="9135220303720555525">"Maak PIP toe"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Volskerm"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-am/strings.xml b/libs/WindowManager/Shell/res/values-am/strings.xml new file mode 100644 index 000000000000..e4628d7b5278 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-am/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"ዝጋ"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"ዘርጋ"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"ቅንብሮች"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"ምናሌ"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> በስዕል-ላይ-ስዕል ውስጥ ነው"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ይህን ባህሪ እንዲጠቀም ካልፈለጉ ቅንብሮችን ለመክፈት መታ ያድርጉና ያጥፉት።"</string> + <string name="pip_play" msgid="3496151081459417097">"አጫውት"</string> + <string name="pip_pause" msgid="690688849510295232">"ባለበት አቁም"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"ወደ ቀጣይ ዝለል"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"ወደ ቀዳሚ ዝለል"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"መጠን ይቀይሩ"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"መተግበሪያ ከተከፈለ ማያ ገጽ ጋር ላይሠራ ይችላል"</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"መተግበሪያው የተከፈለ ማያ ገጽን አይደግፍም።"</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"መተግበሪያ በሁለተኛ ማሳያ ላይ ላይሠራ ይችላል።"</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"መተግበሪያ በሁለተኛ ማሳያዎች ላይ ማስጀመርን አይደግፍም።"</string> + <string name="accessibility_divider" msgid="703810061635792791">"የተከፈለ የማያ ገጽ ከፋይ"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"የግራ ሙሉ ማያ ገጽ"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ግራ 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ግራ 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ግራ 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"የቀኝ ሙሉ ማያ ገጽ"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"የላይ ሙሉ ማያ ገጽ"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ከላይ 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ከላይ 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ከላይ 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"የታች ሙሉ ማያ ገጽ"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"ባለአንድ እጅ ሁነታን በመጠቀም ላይ"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"ለመውጣት ከማያው ግርጌ ወደ ላይ ይጥረጉ ወይም ከመተግበሪያው በላይ ማንኛውም ቦታ ላይ መታ ያድርጉ"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"ባለአንድ እጅ ሁነታ ጀምር"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"ከአንድ እጅ ሁነታ ውጣ"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"ቅንብሮች ለ <xliff:g id="APP_NAME">%1$s</xliff:g> አረፋዎች"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"ትርፍ ፍሰት"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"ወደ ቁልል መልሰው ያክሉ"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ከ<xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ከ <xliff:g id="APP_NAME">%2$s</xliff:g> እና <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> ተጨማሪ"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"ወደ ላይኛው ግራ አንቀሳቅስ"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ወደ ላይኛው ቀኝ አንቀሳቅስ"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"የግርጌውን ግራ አንቀሳቅስ"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ታችኛውን ቀኝ ያንቀሳቅሱ"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"የ<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ቅንብሮች"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"አረፋን አሰናብት"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"ውይይቶችን በአረፋ አታሳይ"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"አረፋዎችን በመጠቀም ይወያዩ"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"አዲስ ውይይቶች እንደ ተንሳፋፊ አዶዎች ወይም አረፋዎች ሆነው ይታያሉ። አረፋን ለመክፈት መታ ያድርጉ። ለመውሰድ ይጎትቱት።"</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"በማንኛውም ጊዜ አረፋዎችን ይቆጣጠሩ"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"የዚህ መተግበሪያ አረፋዎችን ለማጥፋት አቀናብርን መታ ያድርጉ"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"ገባኝ"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"ምንም የቅርብ ጊዜ አረፋዎች የሉም"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"የቅርብ ጊዜ አረፋዎች እና የተሰናበቱ አረፋዎች እዚህ ብቅ ይላሉ"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"አረፋ"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"ያቀናብሩ"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"አረፋ ተሰናብቷል።"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-am/strings_tv.xml b/libs/WindowManager/Shell/res/values-am/strings_tv.xml new file mode 100644 index 000000000000..fcb87c5682e3 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-am/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ስዕል-ላይ-ስዕል"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ርዕስ የሌለው ፕሮግራም)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIPን ዝጋ"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"ሙሉ ማያ ገጽ"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ar/strings.xml b/libs/WindowManager/Shell/res/values-ar/strings.xml new file mode 100644 index 000000000000..7b5bda72ccd4 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ar/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"إغلاق"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"توسيع"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"الإعدادات"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"القائمة"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> يظهر في صورة داخل صورة"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"إذا كنت لا تريد أن يستخدم <xliff:g id="NAME">%s</xliff:g> هذه الميزة، فانقر لفتح الإعدادات، ثم أوقِف تفعيل هذه الميزة."</string> + <string name="pip_play" msgid="3496151081459417097">"تشغيل"</string> + <string name="pip_pause" msgid="690688849510295232">"إيقاف مؤقت"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"التخطي إلى التالي"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"التخطي إلى السابق"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"تغيير الحجم"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"قد لا يعمل التطبيق بشكل سليم في وضع \"تقسيم الشاشة\"."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"التطبيق لا يتيح تقسيم الشاشة."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"قد لا يعمل التطبيق على شاشة عرض ثانوية."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"لا يمكن تشغيل التطبيق على شاشات عرض ثانوية."</string> + <string name="accessibility_divider" msgid="703810061635792791">"أداة تقسيم الشاشة"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"عرض النافذة اليسرى بملء الشاشة"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ضبط حجم النافذة اليسرى ليكون ٧٠%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ضبط حجم النافذة اليسرى ليكون ٥٠%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ضبط حجم النافذة اليسرى ليكون ٣٠%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"عرض النافذة اليمنى بملء الشاشة"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"عرض النافذة العلوية بملء الشاشة"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ضبط حجم النافذة العلوية ليكون ٧٠%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ضبط حجم النافذة العلوية ليكون ٥٠%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ضبط حجم النافذة العلوية ليكون ٣٠%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"عرض النافذة السفلية بملء الشاشة"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"استخدام وضع \"التصفح بيد واحدة\""</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"للخروج، مرِّر سريعًا من أسفل الشاشة إلى أعلاها أو انقر في أي مكان فوق التطبيق."</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"بدء وضع \"التصفح بيد واحدة\""</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"الخروج من وضع \"التصفح بيد واحدة\""</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"إعدادات فقاعات المحادثات على <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"القائمة الكاملة"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"إضافة دعم إلى الحزم"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> من <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> من <xliff:g id="APP_NAME">%2$s</xliff:g> و<xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> أيضًا"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"نقل إلى أعلى يمين الشاشة"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"الانتقال إلى أعلى اليسار"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"نقل إلى أسفل يمين الشاشة"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"نقل إلى أسفل اليسار"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"إعدادات <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"إغلاق فقاعة المحادثة"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"عدم عرض المحادثة كفقاعة محادثة"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"الدردشة باستخدام فقاعات المحادثات"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"تظهر المحادثات الجديدة كرموز عائمة أو كفقاعات. انقر لفتح فقاعة المحادثة، واسحبها لتحريكها."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"التحكّم في فقاعات المحادثات في أي وقت"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"انقر على \"إدارة\" لإيقاف فقاعات المحادثات من هذا التطبيق."</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"حسنًا"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"ليس هناك فقاعات محادثات"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"ستظهر هنا أحدث فقاعات المحادثات وفقاعات المحادثات التي تم إغلاقها."</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"فقاعة"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"إدارة"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"تم إغلاق الفقاعة."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ar/strings_tv.xml b/libs/WindowManager/Shell/res/values-ar/strings_tv.xml new file mode 100644 index 000000000000..4eef29e2ed12 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ar/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"نافذة ضمن النافذة"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ليس هناك عنوان للبرنامج)"</string> + <string name="pip_close" msgid="9135220303720555525">"إغلاق PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"ملء الشاشة"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-as/strings.xml b/libs/WindowManager/Shell/res/values-as/strings.xml new file mode 100644 index 000000000000..47294c438729 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-as/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"বন্ধ কৰক"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"বিস্তাৰ কৰক"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"ছেটিংসমূহ"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"মেনু"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> চিত্ৰৰ ভিতৰৰ চিত্ৰত আছে"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"আপুনি যদি <xliff:g id="NAME">%s</xliff:g> সুবিধাটো ব্যৱহাৰ কৰিব নোখোজে, তেন্তে ছেটিংসমূহ খুলিবলৈ টিপক আৰু তালৈ গৈ ইয়াক অফ কৰক।"</string> + <string name="pip_play" msgid="3496151081459417097">"প্লে কৰক"</string> + <string name="pip_pause" msgid="690688849510295232">"পজ কৰক"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"পৰৱৰ্তী মিডিয়ালৈ যাওক"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"আগৰটো মিডিয়ালৈ যাওক"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"আকাৰ সলনি কৰক"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"এপ্টোৱে বিভাজিত স্ক্ৰীনৰ সৈতে কাম নকৰিব পাৰে।"</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"এপটোৱে বিভাজিত স্ক্ৰীণ সমৰ্থন নকৰে।"</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"গৌণ ডিছপ্লেত এপে সঠিকভাৱে কাম নকৰিব পাৰে।"</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"গৌণ ডিছপ্লেত এপ্ লঞ্চ কৰিব নোৱাৰি।"</string> + <string name="accessibility_divider" msgid="703810061635792791">"স্প্লিট স্ক্ৰীণৰ বিভাজক"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"বাওঁফালৰ স্ক্ৰীণখন সম্পূৰ্ণ স্ক্ৰীণ কৰক"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"বাওঁফালৰ স্ক্ৰীণখন ৭০% কৰক"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"বাওঁফালৰ স্ক্ৰীণখন ৫০% কৰক"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"বাওঁফালৰ স্ক্ৰীণখন ৩০% কৰক"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"সোঁফালৰ স্ক্ৰীণখন সম্পূৰ্ণ স্ক্ৰীণ কৰক"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"শীৰ্ষ স্ক্ৰীণখন সম্পূৰ্ণ স্ক্ৰীণ কৰক"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"শীর্ষ স্ক্ৰীণখন ৭০% কৰক"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"শীর্ষ স্ক্ৰীণখন ৫০% কৰক"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"শীর্ষ স্ক্ৰীণখন ৩০% কৰক"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"তলৰ স্ক্ৰীণখন সম্পূৰ্ণ স্ক্ৰীণ কৰক"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"এখন হাতেৰে ব্যৱহাৰ কৰা ম’ড ব্যৱহাৰ কৰা"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"বাহিৰ হ’বলৈ স্ক্ৰীনখনৰ একেবাৰে তলৰ পৰা ওপৰলৈ ছোৱাইপ কৰক অথবা এপ্টোৰ ওপৰত যিকোনো ঠাইত টিপক"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"এখন হাতেৰে ব্যৱহাৰ কৰা ম\'ডটো আৰম্ভ কৰক"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"এখন হাতেৰে ব্যৱহাৰ কৰা ম\'ডটোৰ পৰা বাহিৰ হওক"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g>ৰ bubblesৰ ছেটিংসমূহ"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"অভাৰফ্ল’"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"ষ্টেকত পুনৰ যোগ দিয়ক"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g>ৰ পৰা <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> আৰু<xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>টাৰ পৰা <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"শীৰ্ষৰ বাওঁফালে নিয়ক"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"শীৰ্ষৰ সোঁফালে নিয়ক"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"বুটামটো বাওঁফালে নিয়ক"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"তলৰ সোঁফালে নিয়ক"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ছেটিংসমূহ"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"বাবল অগ্ৰাহ্য কৰক"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"বাৰ্তালাপ বাবল নকৰিব"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Bubbles ব্যৱহাৰ কৰি চাট কৰক"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"নতুন বাৰ্তালাপ উপঙি থকা চিহ্নসমূহ অথবা bubbles হিচাপে প্ৰদর্শিত হয়। Bubbles খুলিবলৈ টিপক। এইটো স্থানান্তৰ কৰিবলৈ টানি নিয়ক।"</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"যিকোনো সময়তে bubbles নিয়ন্ত্ৰণ কৰক"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"এই এপ্টোৰ পৰা bubbles অফ কৰিবলৈ পৰিচালনা কৰকত টিপক"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"বুজি পালোঁ"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"কোনো শেহতীয়া bubbles নাই"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"শেহতীয়া bubbles আৰু অগ্ৰাহ্য কৰা bubbles ইয়াত প্ৰদর্শিত হ\'ব"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"বাবল"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"পৰিচালনা কৰক"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"বাবল অগ্ৰাহ্য কৰা হৈছে"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-as/strings_tv.xml b/libs/WindowManager/Shell/res/values-as/strings_tv.xml new file mode 100644 index 000000000000..6c223f45d9b3 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-as/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"চিত্ৰৰ ভিতৰত চিত্ৰ"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(শিৰোনামবিহীন কাৰ্যক্ৰম)"</string> + <string name="pip_close" msgid="9135220303720555525">"পিপ বন্ধ কৰক"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"সম্পূৰ্ণ স্ক্ৰীণ"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-az/strings.xml b/libs/WindowManager/Shell/res/values-az/strings.xml new file mode 100644 index 000000000000..923ff79e0627 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-az/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Bağlayın"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Genişləndirin"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Ayarlar"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menyu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> şəkil içində şəkildədir"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> tətbiqinin bu funksiyadan istifadə etməyini istəmirsinizsə, ayarları açmaq və deaktiv etmək üçün klikləyin."</string> + <string name="pip_play" msgid="3496151081459417097">"Oxudun"</string> + <string name="pip_pause" msgid="690688849510295232">"Fasilə verin"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Növbətiyə keçin"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Əvvəlkinə keçin"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ölçüsünü dəyişin"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Tətbiq bölünmüş ekran ilə işləməyə bilər."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Tətbiq ekran bölünməsini dəstəkləmir."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Tətbiq ikinci ekranda işləməyə bilər."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Tətbiq ikinci ekranda başlamağı dəstəkləmir."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Bölünmüş ekran ayırıcısı"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Sol tam ekran"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Sol 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Sol 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Sol 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Sağ tam ekran"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Yuxarı tam ekran"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Yuxarı 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Yuxarı 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Yuxarı 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Aşağı tam ekran"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Bir əlli rejimdən istifadə edilir"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Çıxmaq üçün ekranın aşağısından yuxarıya doğru sürüşdürün və ya tətbiqin yuxarısında istənilən yerə toxunun"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Bir əlli rejimi başladın"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Bir əlli rejimdən çıxın"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> yumrucuqları üçün ayarlar"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Kənara çıxma"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Yenidən dəstəyə əlavə edin"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g> tətbiqindən <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> tətbiqindən <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> və daha <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> qabarcıq"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Yuxarıya sola köçürün"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Yuxarıya sağa köçürün"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Aşağıya sola köçürün"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Aşağıya sağa köçürün"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ayarları"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Yumrucuğu ləğv edin"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Söhbəti yumrucuqda göstərmə"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Yumrucuqlardan istifadə edərək söhbət edin"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Yeni söhbətlər üzən nişanlar və ya yumrucuqlar kimi görünür. Yumrucuğu açmaq üçün toxunun. Hərəkət etdirmək üçün sürüşdürün."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Yumrucuqları istənilən vaxt idarə edin"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Bu tətbiqdə yumrucuqları deaktiv etmək üçün \"İdarə edin\" seçiminə toxunun"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Anladım"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Yumrucuqlar yoxdur"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Son yumrucuqlar və buraxılmış yumrucuqlar burada görünəcək"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Qabarcıq"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"İdarə edin"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Qabarcıqdan imtina edilib."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-az/strings_tv.xml b/libs/WindowManager/Shell/res/values-az/strings_tv.xml new file mode 100644 index 000000000000..c9f1acbef31b --- /dev/null +++ b/libs/WindowManager/Shell/res/values-az/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Şəkil-içində-Şəkil"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Başlıqsız proqram)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP bağlayın"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Tam ekran"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml new file mode 100644 index 000000000000..02e609cd5c9b --- /dev/null +++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Zatvori"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Proširi"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Podešavanja"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Meni"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> je slika u slici"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Ako ne želite da <xliff:g id="NAME">%s</xliff:g> koristi ovu funkciju, dodirnite da biste otvorili podešavanja i isključili je."</string> + <string name="pip_play" msgid="3496151081459417097">"Pusti"</string> + <string name="pip_pause" msgid="690688849510295232">"Pauziraj"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Pređi na sledeće"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Pređi na prethodno"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Promenite veličinu"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacija možda neće raditi sa podeljenim ekranom."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podržava podeljeni ekran."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija možda neće funkcionisati na sekundarnom ekranu."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacija ne podržava pokretanje na sekundarnim ekranima."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Razdelnik podeljenog ekrana"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Režim celog ekrana za levi ekran"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Levi ekran 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Levi ekran 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Levi ekran 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Režim celog ekrana za donji ekran"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Režim celog ekrana za gornji ekran"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Gornji ekran 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Gornji ekran 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Gornji ekran 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Režim celog ekrana za donji ekran"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Korišćenje režima jednom rukom"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Da biste izašli, prevucite nagore od dna ekrana ili dodirnite bilo gde iznad aplikacije"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Pokrenite režim jednom rukom"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Izađite iz režima jednom rukom"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Podešavanja za <xliff:g id="APP_NAME">%1$s</xliff:g> oblačiće"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Preklapanje"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Dodaj ponovo u grupu"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> iz aplikacije <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> iz aplikacije <xliff:g id="APP_NAME">%2$s</xliff:g> i još <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Premesti gore levo"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Premesti gore desno"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Premesti dole levo"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Premesti dole desno"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Podešavanja za <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Odbaci oblačić"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ne koristi oblačiće za konverzaciju"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Ćaskajte u oblačićima"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nove konverzacije se prikazuju kao plutajuće ikone ili oblačići. Dodirnite da biste otvorili oblačić. Prevucite da biste ga premestili."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Kontrolišite oblačiće u bilo kom trenutku"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Dodirnite Upravljajte da biste isključili oblačiće iz ove aplikacije"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Važi"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Nema nedavnih oblačića"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Ovde se prikazuju nedavni i odbačeni oblačići"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Oblačić"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljajte"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblačić je odbačen."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml new file mode 100644 index 000000000000..6fbc91bbec60 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Slika u slici"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez naslova)"</string> + <string name="pip_close" msgid="9135220303720555525">"Zatvori PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Ceo ekran"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-be/strings.xml b/libs/WindowManager/Shell/res/values-be/strings.xml new file mode 100644 index 000000000000..ccea3180f64e --- /dev/null +++ b/libs/WindowManager/Shell/res/values-be/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Закрыць"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Разгарнуць"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Налады"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Меню"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> з’яўляецца відарысам у відарысе"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Калі вы не хочаце, каб праграма <xliff:g id="NAME">%s</xliff:g> выкарыстоўвала гэту функцыю, дакраніцеся, каб адкрыць налады і адключыць яе."</string> + <string name="pip_play" msgid="3496151081459417097">"Прайграць"</string> + <string name="pip_pause" msgid="690688849510295232">"Прыпыніць"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Перайсці да наступнага"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Перайсці да папярэдняга"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Змяніць памер"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Праграма можа не працаваць у рэжыме падзеленага экрана."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Праграма не падтрымлівае функцыю дзялення экрана."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Праграма можа не працаваць на дадатковых экранах."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Праграма не падтрымлівае запуск на дадатковых экранах."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Раздзяляльнік падзеленага экрана"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Левы экран – поўнаэкранны рэжым"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Левы экран – 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Левы экран – 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Левы экран – 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Правы экран – поўнаэкранны рэжым"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Верхні экран – поўнаэкранны рэжым"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Верхні экран – 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Верхні экран – 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Верхні экран – 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Ніжні экран – поўнаэкранны рэжым"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Выкарыстоўваецца рэжым кіравання адной рукой"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Каб выйсці, правядзіце па экране пальцам знізу ўверх або націсніце ў любым месцы над праграмай"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Запусціць рэжым кіравання адной рукой"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Выйсці з рэжыму кіравання адной рукой"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Налады ўсплывальных апавяшчэнняў у праграме \"<xliff:g id="APP_NAME">%1$s</xliff:g>\""</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Дадатковае меню"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Зноў дадаць у стос"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ад праграмы \"<xliff:g id="APP_NAME">%2$s</xliff:g>\""</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ад праграмы \"<xliff:g id="APP_NAME">%2$s</xliff:g>\" і яшчэ <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Перамясціць лявей і вышэй"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Перамясціце правей і вышэй"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Перамясціць лявей і ніжэй"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Перамясціць правей і ніжэй"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Налады \"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>\""</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Адхіліць апавяшчэнне"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Не паказваць размову ў выглядзе ўсплывальных апавяшчэнняў"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Усплывальныя апавяшчэнні"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Новыя размовы будуць паказвацца як рухомыя значкі ці ўсплывальныя апавяшчэнні. Націсніце, каб адкрыць усплывальнае апавяшчэнне. Перацягніце яго, каб перамясціць."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Кіруйце ўсплывальнымі апавяшчэннямі ў любы час"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Каб выключыць усплывальныя апавяшчэнні з гэтай праграмы, націсніце \"Кіраваць\""</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Зразумела"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Няма нядаўніх усплывальных апавяшчэнняў"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Нядаўнія і адхіленыя ўсплывальныя апавяшчэнні будуць паказаны тут"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Усплывальнае апавяшчэнне"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Кіраваць"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Усплывальнае апавяшчэнне адхілена."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-be/strings_tv.xml b/libs/WindowManager/Shell/res/values-be/strings_tv.xml new file mode 100644 index 000000000000..d33bf99e2ebd --- /dev/null +++ b/libs/WindowManager/Shell/res/values-be/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Відарыс у відарысе"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Праграма без назвы)"</string> + <string name="pip_close" msgid="9135220303720555525">"Закрыць PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Поўнаэкранны рэжым"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-bg/strings.xml b/libs/WindowManager/Shell/res/values-bg/strings.xml new file mode 100644 index 000000000000..d29660b9c24d --- /dev/null +++ b/libs/WindowManager/Shell/res/values-bg/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Затваряне"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Разгъване"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Настройки"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Меню"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> е в режима „Картина в картината“"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Ако не искате <xliff:g id="NAME">%s</xliff:g> да използва тази функция, докоснете, за да отворите настройките, и я изключете."</string> + <string name="pip_play" msgid="3496151081459417097">"Пускане"</string> + <string name="pip_pause" msgid="690688849510295232">"Поставяне на пауза"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Към следващия елемент"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Към предишния елемент"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Преоразмеряване"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Приложението може да не работи в режим на разделен екран."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Приложението не поддържа разделен екран."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Възможно е приложението да не работи на алтернативни дисплеи."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Приложението не поддържа използването на алтернативни дисплеи."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Разделител в режима за разделен екран"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ляв екран: Показване на цял екран"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Ляв екран: 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Ляв екран: 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Ляв екран: 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Десен екран: Показване на цял екран"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Горен екран: Показване на цял екран"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Горен екран: 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Горен екран: 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Горен екран: 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Долен екран: Показване на цял екран"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Използване на режима за работа с една ръка"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"За изход прекарайте пръст нагоре от долната част на екрана или докоснете произволно място над приложението"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Стартиране на режима за работа с една ръка"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Изход от режима за работа с една ръка"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Настройки за балончетата за <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Препълване"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Добавяне обратно към стека"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> от <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"„<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>“ от<xliff:g id="APP_NAME">%2$s</xliff:g> и още <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Преместване горе вляво"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Преместване горе вдясно"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Преместване долу вляво"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Преместване долу вдясно"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Настройки за <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Отхвърляне на балончетата"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Без балончета за разговора"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Чат с балончета"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Новите разговори се показват като плаващи икони, или балончета. Докоснете балонче, за да го отворите, или го плъзнете, за да го преместите."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Управление на балончетата по всяко време"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Докоснете „Управление“, за да изключите балончетата от това приложение"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Разбрах"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Няма скорошни балончета"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Скорошните и отхвърлените балончета ще се показват тук"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Балонче"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Управление"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Балончето е отхвърлено."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-bg/strings_tv.xml b/libs/WindowManager/Shell/res/values-bg/strings_tv.xml new file mode 100644 index 000000000000..f4fad601179f --- /dev/null +++ b/libs/WindowManager/Shell/res/values-bg/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Картина в картината"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Програма без заглавие)"</string> + <string name="pip_close" msgid="9135220303720555525">"Затваряне на PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Цял екран"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-bn/strings.xml b/libs/WindowManager/Shell/res/values-bn/strings.xml new file mode 100644 index 000000000000..84bcaf907d91 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-bn/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"বন্ধ করুন"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"বড় করুন"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"সেটিংস"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"মেনু"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"ছবির-মধ্যে-ছবি তে <xliff:g id="NAME">%s</xliff:g> আছেন"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> কে এই বৈশিষ্ট্যটি ব্যবহার করতে দিতে না চাইলে ট্যাপ করে সেটিংসে গিয়ে সেটি বন্ধ করে দিন।"</string> + <string name="pip_play" msgid="3496151081459417097">"চালান"</string> + <string name="pip_pause" msgid="690688849510295232">"বিরাম দিন"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"এগিয়ে যাওয়ার জন্য এড়িয়ে যান"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"পিছনে যাওয়ার জন্য এড়িয়ে যান"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"রিসাইজ করুন"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"অ্যাপটি স্প্লিট স্ক্রিনে কাজ নাও করতে পারে।"</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"অ্যাপ্লিকেশান বিভক্ত-স্ক্রিন সমর্থন করে না৷"</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"সেকেন্ডারি ডিসপ্লেতে অ্যাপটি কাজ নাও করতে পারে।"</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"সেকেন্ডারি ডিসপ্লেতে অ্যাপ লঞ্চ করা যাবে না।"</string> + <string name="accessibility_divider" msgid="703810061635792791">"বিভক্ত-স্ক্রিন বিভাজক"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"বাঁ দিকের অংশ নিয়ে পূর্ণ স্ক্রিন"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"৭০% বাকি আছে"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"৫০% বাকি আছে"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"৩০% বাকি আছে"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"ডান দিকের অংশ নিয়ে পূর্ণ স্ক্রিন"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"উপর দিকের অংশ নিয়ে পূর্ণ স্ক্রিন"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"শীর্ষ ৭০%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"শীর্ষ ৫০%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"শীর্ষ ৩০%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"নীচের অংশ নিয়ে পূর্ণ স্ক্রিন"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"\'এক হাতে ব্যবহার করার মোড\'-এর ব্যবহার"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"বেরিয়ে আসার জন্য, স্ক্রিনের নিচ থেকে উপরের দিকে সোয়াইপ করুন অথবা অ্যাপ আইকনের উপরে যেকোনও জায়গায় ট্যাপ করুন"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"\'এক হাতে ব্যবহার করার মোড\' শুরু করুন"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"\'এক হাতে ব্যবহার করার মোড\' থেকে বেরিয়ে আসুন"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> বাবলের জন্য সেটিংস"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"ওভারফ্লো"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"স্ট্যাকে আবার যোগ করুন"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g> অ্যাপ থেকে <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> অ্যাপ এবং আরও <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>টি থেকে <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"উপরে বাঁদিকে সরান"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"উপরে ডানদিকে সরান"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"নিচে বাঁদিকে সরান"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"নিচে ডান দিকে সরান"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> সেটিংস"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"বাবল খারিজ করুন"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"কথোপকথন বাবল হিসেবে দেখাবে না"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"বাবল ব্যবহার করে চ্যাট করুন"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"নতুন কথোপকথন ভেসে থাকা আইকন বা বাবল হিসেবে দেখানো হয়। বাবল খুলতে ট্যাপ করুন। সেটি সরাতে ধরে টেনে আনুন।"</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"যেকোনও সময় বাবল নিয়ন্ত্রণ করুন"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"এই অ্যাপ থেকে বাবল বন্ধ করতে \'ম্যানেজ করুন\' বিকল্প ট্যাপ করুন"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"বুঝেছি"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"কোনও সাম্প্রতিক বাবল নেই"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"সাম্প্রতিক ও বাতিল করা বাবল এখানে দেখা যাবে"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"বাবল"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"ম্যানেজ করুন"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"বাবল বাতিল করা হয়েছে।"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-bn/strings_tv.xml b/libs/WindowManager/Shell/res/values-bn/strings_tv.xml new file mode 100644 index 000000000000..0eb83a0276e6 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-bn/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ছবির-মধ্যে-ছবি"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(শিরোনামহীন প্রোগ্রাম)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP বন্ধ করুন"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"পূর্ণ স্ক্রিন"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml new file mode 100644 index 000000000000..85e08d7ca555 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-bs/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Zatvori"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Proširi"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Postavke"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Meni"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> je u načinu priakza Slika u slici"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Ako ne želite da <xliff:g id="NAME">%s</xliff:g> koristi ovu funkciju, dodirnite da otvorite postavke i isključite je."</string> + <string name="pip_play" msgid="3496151081459417097">"Reproduciraj"</string> + <string name="pip_pause" msgid="690688849510295232">"Pauziraj"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Preskoči na sljedeći"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Preskoči na prethodni"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Promjena veličine"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacija možda neće raditi na podijeljenom ekranu."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podržava dijeljenje ekrana."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija možda neće raditi na sekundarnom ekranu."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacija ne podržava pokretanje na sekundarnim ekranima."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Razdjelnik ekrana"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Lijevo cijeli ekran"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Lijevo 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Lijevo 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Lijevo 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Desno cijeli ekran"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Gore cijeli ekran"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Gore 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Gore 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Gore 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Donji ekran kao cijeli ekran"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Korištenje načina rada jednom rukom"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Da izađete, prevucite s dna ekrana prema gore ili dodirnite bilo gdje iznad aplikacije"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Započinjanje načina rada jednom rukom"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Izlaz iz načina rada jednom rukom"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Postavke za oblačiće aplikacije <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Preklapanje"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Dodaj nazad u grupu"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> od aplikacije <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"Obavještenje <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> aplikacije <xliff:g id="APP_NAME">%2$s</xliff:g> i još <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Pomjeri gore lijevo"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Pomjerite gore desno"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Pomjeri dolje lijevo"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Pomjerite dolje desno"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Postavke aplikacije <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Odbaci oblačić"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nemoj prikazivati razgovor u oblačićima"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatajte koristeći oblačiće"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Novi razgovori se prikazuju kao plutajuće ikone ili oblačići. Dodirnite da otvorite oblačić. Prevucite da ga premjestite."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Upravljajte oblačićima u svakom trenutku"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Dodirnite Upravljaj da isključite oblačiće iz ove aplikacije"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Razumijem"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Nema nedavnih oblačića"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Nedavni i odbačeni oblačići će se pojaviti ovdje"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Oblačić"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljaj"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblačić je odbačen."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-bs/strings_tv.xml b/libs/WindowManager/Shell/res/values-bs/strings_tv.xml new file mode 100644 index 000000000000..8e301b0a8f4d --- /dev/null +++ b/libs/WindowManager/Shell/res/values-bs/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Slika u slici"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez naslova)"</string> + <string name="pip_close" msgid="9135220303720555525">"Zatvori PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Cijeli ekran"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ca/strings.xml b/libs/WindowManager/Shell/res/values-ca/strings.xml new file mode 100644 index 000000000000..a80b7fbec09a --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ca/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Tanca"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Desplega"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Configuració"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menú"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> està en pantalla en pantalla"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Si no vols que <xliff:g id="NAME">%s</xliff:g> utilitzi aquesta funció, toca per obrir la configuració i desactiva-la."</string> + <string name="pip_play" msgid="3496151081459417097">"Reprodueix"</string> + <string name="pip_pause" msgid="690688849510295232">"Posa en pausa"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Ves al següent"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Torna a l\'anterior"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Canvia la mida"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"És possible que l\'aplicació no funcioni amb la pantalla dividida."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"L\'aplicació no admet la pantalla dividida."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"És possible que l\'aplicació no funcioni en una pantalla secundària."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"L\'aplicació no es pot obrir en pantalles secundàries."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Divisor de pantalles"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Pantalla esquerra completa"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Pantalla esquerra al 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Pantalla esquerra al 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Pantalla esquerra al 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Pantalla dreta completa"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Pantalla superior completa"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Pantalla superior al 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Pantalla superior al 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Pantalla superior al 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Pantalla inferior completa"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"S\'està utilitzant el mode d\'una mà"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Per sortir, llisca cap amunt des de la part inferior de la pantalla o toca qualsevol lloc a sobre de l\'aplicació"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Inicia el mode d\'una mà"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Surt del mode d\'una mà"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Configuració de les bombolles: <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Menú addicional"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Torna a afegir a la pila"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de: <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> (<xliff:g id="APP_NAME">%2$s</xliff:g>) i <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> més"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mou a dalt a l\'esquerra"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mou a dalt a la dreta"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mou a baix a l\'esquerra"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mou a baix a la dreta"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Configuració de l\'aplicació <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ignora la bombolla"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"No mostris la conversa com a bombolla"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Xateja amb bombolles"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Les converses noves es mostren com a icones flotants o bombolles. Toca per obrir una bombolla. Arrossega-la per moure-la."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Controla les bombolles en qualsevol moment"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Toca Gestiona per desactivar les bombolles d\'aquesta aplicació"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Entesos"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"No hi ha bombolles recents"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Les bombolles recents i les ignorades es mostraran aquí"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Bombolla"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestiona"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"La bombolla s\'ha ignorat."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ca/strings_tv.xml b/libs/WindowManager/Shell/res/values-ca/strings_tv.xml new file mode 100644 index 000000000000..b80fc41402dd --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ca/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pantalla en pantalla"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa sense títol)"</string> + <string name="pip_close" msgid="9135220303720555525">"Tanca PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-cs/strings.xml b/libs/WindowManager/Shell/res/values-cs/strings.xml new file mode 100644 index 000000000000..e8257bc8ee92 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-cs/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Zavřít"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Rozbalit"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Nastavení"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Nabídka"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"Aplikace <xliff:g id="NAME">%s</xliff:g> je v režimu obraz v obraze"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Pokud nechcete, aby aplikace <xliff:g id="NAME">%s</xliff:g> tuto funkci používala, klepnutím otevřete nastavení a funkci vypněte."</string> + <string name="pip_play" msgid="3496151081459417097">"Přehrát"</string> + <string name="pip_pause" msgid="690688849510295232">"Pozastavit"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Přeskočit na další"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Přeskočit na předchozí"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Změnit velikost"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikace v režimu rozdělené obrazovky nemusí fungovat."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikace nepodporuje režim rozdělené obrazovky."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikace na sekundárním displeji nemusí fungovat."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikace nepodporuje spuštění na sekundárních displejích."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Čára rozdělující obrazovku"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Levá část na celou obrazovku"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70 % vlevo"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50 % vlevo"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"30 % vlevo"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Pravá část na celou obrazovku"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Horní část na celou obrazovku"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"70 % nahoře"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50 % nahoře"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"30 % nahoře"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Dolní část na celou obrazovku"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Používání režimu jedné ruky"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Režim ukončíte, když přejedete prstem z dolní části obrazovky nahoru nebo klepnete kamkoli nad aplikaci"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Spustit režim jedné ruky"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Ukončit režim jedné ruky"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Nastavení bublin aplikace <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Rozbalovací nabídka"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Přidat zpět do sady"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"Oznámení <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> z aplikace <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> z aplikace <xliff:g id="APP_NAME">%2$s</xliff:g> a dalších (<xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>)"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Přesunout vlevo nahoru"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Přesunout vpravo nahoru"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Přesunout vlevo dolů"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Přesunout vpravo dolů"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Nastavení <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Zavřít bublinu"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nezobrazovat konverzaci v bublinách"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatujte pomocí bublin"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nové konverzace se zobrazují jako plovoucí ikony, neboli bubliny. Klepnutím bublinu otevřete. Přetažením ji posunete."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Nastavení bublin můžete kdykoli upravit"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Bubliny pro tuto aplikaci můžete vypnout klepnutím na Spravovat"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Žádné nedávné bubliny"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Zde se budou zobrazovat nedávné bubliny a zavřené bubliny"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Bublina"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Spravovat"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bublina byla zavřena."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-cs/strings_tv.xml b/libs/WindowManager/Shell/res/values-cs/strings_tv.xml new file mode 100644 index 000000000000..56abcbe473fb --- /dev/null +++ b/libs/WindowManager/Shell/res/values-cs/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Obraz v obraze"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Bez názvu)"</string> + <string name="pip_close" msgid="9135220303720555525">"Ukončit obraz v obraze (PIP)"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Celá obrazovka"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml new file mode 100644 index 000000000000..17f8286e8069 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-da/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Luk"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Udvid"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Indstillinger"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> vises som integreret billede"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Hvis du ikke ønsker, at <xliff:g id="NAME">%s</xliff:g> skal benytte denne funktion, kan du åbne indstillingerne og deaktivere den."</string> + <string name="pip_play" msgid="3496151081459417097">"Afspil"</string> + <string name="pip_pause" msgid="690688849510295232">"Sæt på pause"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Gå videre til næste"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Gå til forrige"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Rediger størrelse"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Appen fungerer muligvis ikke i opdelt skærm."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Appen understøtter ikke opdelt skærm."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Appen fungerer muligvis ikke på sekundære skærme."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Appen kan ikke åbnes på sekundære skærme."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Adskiller til opdelt skærm"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Vis venstre del i fuld skærm"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Venstre 70 %"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Venstre 50 %"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Venstre 30 %"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Vis højre del i fuld skærm"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Vis øverste del i fuld skærm"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Øverste 70 %"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Øverste 50 %"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Øverste 30 %"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Vis nederste del i fuld skærm"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Brug af enhåndstilstand"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Du kan afslutte ved at stryge opad fra bunden af skærmen eller trykke et vilkårligt sted over appen"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Start enhåndstilstand"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Afslut enhåndstilstand"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Indstillinger for <xliff:g id="APP_NAME">%1$s</xliff:g>-bobler"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Overløb"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Føj til stak igen"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> fra <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> fra <xliff:g id="APP_NAME">%2$s</xliff:g> og <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> andre"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Flyt op til venstre"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Flyt op til højre"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Flyt ned til venstre"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Flyt ned til højre"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Indstillinger for <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Afvis boble"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Vis ikke samtaler i bobler"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat ved hjælp af bobler"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nye samtaler vises som svævende ikoner eller bobler. Tryk for at åbne boblen. Træk for at flytte den."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Styr bobler når som helst"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Tryk på Administrer for at deaktivere bobler fra denne app"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Ingen seneste bobler"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Nye bobler og afviste bobler vises her"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Boble"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Administrer"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Boblen blev lukket."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-da/strings_tv.xml b/libs/WindowManager/Shell/res/values-da/strings_tv.xml new file mode 100644 index 000000000000..fdb6b783399e --- /dev/null +++ b/libs/WindowManager/Shell/res/values-da/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Integreret billede"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program uden titel)"</string> + <string name="pip_close" msgid="9135220303720555525">"Luk integreret billede"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Fuld skærm"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-de/strings.xml b/libs/WindowManager/Shell/res/values-de/strings.xml new file mode 100644 index 000000000000..f04796aca753 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-de/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Schließen"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Maximieren"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Einstellungen"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menü"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ist in Bild im Bild"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Wenn du nicht möchtest, dass <xliff:g id="NAME">%s</xliff:g> diese Funktion verwendet, tippe, um die Einstellungen zu öffnen und die Funktion zu deaktivieren."</string> + <string name="pip_play" msgid="3496151081459417097">"Wiedergeben"</string> + <string name="pip_pause" msgid="690688849510295232">"Pausieren"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Vorwärts springen"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Rückwärts springen"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Größe anpassen"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Die App funktioniert unter Umständen bei geteiltem Bildschirmmodus nicht."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Das Teilen des Bildschirms wird in dieser App nicht unterstützt."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Die App funktioniert auf einem sekundären Display möglicherweise nicht."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Die App unterstützt den Start auf sekundären Displays nicht."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Bildschirmteiler"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Vollbild links"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70 % links"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50 % links"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"30 % links"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Vollbild rechts"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Vollbild oben"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"70 % oben"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50 % oben"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"30 % oben"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Vollbild unten"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Einhandmodus wird verwendet"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Wenn du die App schließen möchtest, wische vom unteren Rand des Displays nach oben oder tippe auf eine beliebige Stelle oberhalb der App"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Einhandmodus starten"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Einhandmodus beenden"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Einstellungen für <xliff:g id="APP_NAME">%1$s</xliff:g>-Bubbles"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Mehr anzeigen"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Wieder dem Stapel hinzufügen"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> von <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> aus <xliff:g id="APP_NAME">%2$s</xliff:g> und <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> weiteren"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Nach oben links verschieben"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Nach rechts oben verschieben"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Nach unten links verschieben"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Nach unten rechts verschieben"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Einstellungen für <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Bubble schließen"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Unterhaltung nicht als Bubble anzeigen"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Bubbles zum Chatten verwenden"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Neue Unterhaltungen erscheinen als unverankerte Symbole, \"Bubbles\" genannt. Wenn du die Bubble öffnen möchtest, tippe sie an. Wenn du sie verschieben möchtest, zieh an ihr."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Bubble-Einstellungen festlegen"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Tippe auf \"Verwalten\", um Bubbles für diese App zu deaktivieren"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Keine kürzlich geschlossenen Bubbles"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Hier werden aktuelle und geschlossene Bubbles angezeigt"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Verwalten"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble verworfen."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-de/strings_tv.xml b/libs/WindowManager/Shell/res/values-de/strings_tv.xml new file mode 100644 index 000000000000..02cce9d73647 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-de/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Bild im Bild"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Kein Sendungsname gefunden)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP schließen"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Vollbild"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-el/strings.xml b/libs/WindowManager/Shell/res/values-el/strings.xml new file mode 100644 index 000000000000..cc329e8f3274 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-el/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Κλείσιμο"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Ανάπτυξη"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Ρυθμίσεις"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Μενού"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"Η λειτουργία picture-in-picture είναι ενεργή σε <xliff:g id="NAME">%s</xliff:g>."</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Εάν δεν θέλετε να χρησιμοποιείται αυτή η λειτουργία από την εφαρμογή <xliff:g id="NAME">%s</xliff:g>, πατήστε για να ανοίξετε τις ρυθμίσεις και απενεργοποιήστε την."</string> + <string name="pip_play" msgid="3496151081459417097">"Αναπαραγωγή"</string> + <string name="pip_pause" msgid="690688849510295232">"Παύση"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Μετάβαση στο επόμενο"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Μετάβαση στο προηγούμενο"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Αλλαγή μεγέθους"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Η εφαρμογή ενδέχεται να μην λειτουργεί με διαχωρισμό οθόνης."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Η εφαρμογή δεν υποστηρίζει διαχωρισμό οθόνης."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Η εφαρμογή ίσως να μην λειτουργήσει σε δευτερεύουσα οθόνη."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Η εφαρμογή δεν υποστηρίζει την εκκίνηση σε δευτερεύουσες οθόνες."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Διαχωριστικό οθόνης"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Αριστερή πλήρης οθόνη"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Αριστερή 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Αριστερή 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Αριστερή 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Δεξιά πλήρης οθόνη"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Πάνω πλήρης οθόνη"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Πάνω 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Πάνω 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Πάνω 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Κάτω πλήρης οθόνη"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Χρήση λειτουργίας ενός χεριού"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Για έξοδο, σύρετε προς τα πάνω από το κάτω μέρος της οθόνης ή πατήστε οπουδήποτε πάνω από την εφαρμογή."</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Έναρξη λειτουργίας ενός χεριού"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"\'Έξοδος από τη λειτουργία ενός χεριού"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Ρυθμίσεις για συννεφάκια <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Υπερχείλιση"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Προσθήκη ξανά στη στοίβα"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> από <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> από την εφαρμογή <xliff:g id="APP_NAME">%2$s</xliff:g> και <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> ακόμη"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Μετακίνηση επάνω αριστερά"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Μετακίνηση επάνω δεξιά"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Μετακίνηση κάτω αριστερά"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Μετακίνηση κάτω δεξιά"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Ρυθμίσεις <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Παράβλ. για συννεφ."</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Να μην γίνει προβολή της συζήτησης σε συννεφάκια."</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Συζητήστε χρησιμοποιώντας συννεφάκια."</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Οι νέες συζητήσεις εμφανίζονται ως κινούμενα εικονίδια ή συννεφάκια. Πατήστε για να ανοίξετε το συννεφάκι. Σύρετε για να το μετακινήσετε."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Ελέγξτε τα συννεφάκια ανά πάσα στιγμή."</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Πατήστε Διαχείριση για να απενεργοποιήσετε τα συννεφάκια από αυτήν την εφαρμογή."</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Το κατάλαβα"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Δεν υπάρχουν πρόσφατα συννεφάκια"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Τα πρόσφατα συννεφάκια και τα συννεφάκια που παραβλέψατε θα εμφανίζονται εδώ."</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Συννεφάκι"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Διαχείριση"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Το συννεφάκι παραβλέφθηκε."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-el/strings_tv.xml b/libs/WindowManager/Shell/res/values-el/strings_tv.xml new file mode 100644 index 000000000000..880ea37e6bf7 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-el/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-Picture"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Δεν υπάρχει τίτλος προγράμματος)"</string> + <string name="pip_close" msgid="9135220303720555525">"Κλείσιμο PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Πλήρης οθόνη"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml new file mode 100644 index 000000000000..90c71c0e11ea --- /dev/null +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Close"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Expand"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string> + <string name="pip_play" msgid="3496151081459417097">"Play"</string> + <string name="pip_pause" msgid="690688849510295232">"Pause"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Skip to next"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Skip to previous"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Resize"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App does not support launch on secondary displays."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Split screen divider"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Left full screen"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Left 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Left 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Left 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Right full screen"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Top full screen"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Top 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Top 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Top 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Bottom full screen"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Using one-handed mode"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"To exit, swipe up from the bottom of the screen or tap anywhere above the app"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Start one-handed mode"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Exit one-handed mode"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Settings for <xliff:g id="APP_NAME">%1$s</xliff:g> bubbles"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Overflow"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Add back to stack"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> from <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> from <xliff:g id="APP_NAME">%2$s</xliff:g> and <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> more"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Move top left"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Move top right"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Move bottom left"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Move bottom right"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> settings"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dismiss bubble"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Don’t bubble conversation"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat using bubbles"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"New conversations appear as floating icons, or bubbles. Tap to open bubble. Drag to move it."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Control bubbles at any time"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Tap Manage to turn off bubbles from this app"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"No recent bubbles"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Recent bubbles and dismissed bubbles will appear here"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml new file mode 100644 index 000000000000..e3f08c8cc76f --- /dev/null +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string> + <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml new file mode 100644 index 000000000000..90c71c0e11ea --- /dev/null +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Close"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Expand"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string> + <string name="pip_play" msgid="3496151081459417097">"Play"</string> + <string name="pip_pause" msgid="690688849510295232">"Pause"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Skip to next"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Skip to previous"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Resize"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App does not support launch on secondary displays."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Split screen divider"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Left full screen"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Left 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Left 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Left 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Right full screen"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Top full screen"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Top 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Top 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Top 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Bottom full screen"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Using one-handed mode"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"To exit, swipe up from the bottom of the screen or tap anywhere above the app"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Start one-handed mode"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Exit one-handed mode"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Settings for <xliff:g id="APP_NAME">%1$s</xliff:g> bubbles"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Overflow"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Add back to stack"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> from <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> from <xliff:g id="APP_NAME">%2$s</xliff:g> and <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> more"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Move top left"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Move top right"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Move bottom left"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Move bottom right"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> settings"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dismiss bubble"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Don’t bubble conversation"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat using bubbles"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"New conversations appear as floating icons, or bubbles. Tap to open bubble. Drag to move it."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Control bubbles at any time"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Tap Manage to turn off bubbles from this app"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"No recent bubbles"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Recent bubbles and dismissed bubbles will appear here"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml new file mode 100644 index 000000000000..e3f08c8cc76f --- /dev/null +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string> + <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml new file mode 100644 index 000000000000..90c71c0e11ea --- /dev/null +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Close"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Expand"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string> + <string name="pip_play" msgid="3496151081459417097">"Play"</string> + <string name="pip_pause" msgid="690688849510295232">"Pause"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Skip to next"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Skip to previous"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Resize"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App does not support launch on secondary displays."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Split screen divider"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Left full screen"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Left 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Left 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Left 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Right full screen"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Top full screen"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Top 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Top 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Top 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Bottom full screen"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Using one-handed mode"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"To exit, swipe up from the bottom of the screen or tap anywhere above the app"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Start one-handed mode"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Exit one-handed mode"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Settings for <xliff:g id="APP_NAME">%1$s</xliff:g> bubbles"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Overflow"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Add back to stack"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> from <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> from <xliff:g id="APP_NAME">%2$s</xliff:g> and <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> more"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Move top left"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Move top right"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Move bottom left"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Move bottom right"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> settings"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dismiss bubble"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Don’t bubble conversation"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat using bubbles"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"New conversations appear as floating icons, or bubbles. Tap to open bubble. Drag to move it."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Control bubbles at any time"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Tap Manage to turn off bubbles from this app"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"No recent bubbles"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Recent bubbles and dismissed bubbles will appear here"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml new file mode 100644 index 000000000000..e3f08c8cc76f --- /dev/null +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string> + <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml new file mode 100644 index 000000000000..90c71c0e11ea --- /dev/null +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Close"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Expand"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string> + <string name="pip_play" msgid="3496151081459417097">"Play"</string> + <string name="pip_pause" msgid="690688849510295232">"Pause"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Skip to next"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Skip to previous"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Resize"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App does not support launch on secondary displays."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Split screen divider"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Left full screen"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Left 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Left 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Left 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Right full screen"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Top full screen"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Top 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Top 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Top 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Bottom full screen"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Using one-handed mode"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"To exit, swipe up from the bottom of the screen or tap anywhere above the app"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Start one-handed mode"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Exit one-handed mode"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Settings for <xliff:g id="APP_NAME">%1$s</xliff:g> bubbles"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Overflow"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Add back to stack"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> from <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> from <xliff:g id="APP_NAME">%2$s</xliff:g> and <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> more"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Move top left"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Move top right"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Move bottom left"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Move bottom right"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> settings"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dismiss bubble"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Don’t bubble conversation"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat using bubbles"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"New conversations appear as floating icons, or bubbles. Tap to open bubble. Drag to move it."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Control bubbles at any time"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Tap Manage to turn off bubbles from this app"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"No recent bubbles"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Recent bubbles and dismissed bubbles will appear here"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml new file mode 100644 index 000000000000..e3f08c8cc76f --- /dev/null +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string> + <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml new file mode 100644 index 000000000000..d8b5b40035f7 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-en-rXC/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Close"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Expand"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string> + <string name="pip_play" msgid="3496151081459417097">"Play"</string> + <string name="pip_pause" msgid="690688849510295232">"Pause"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Skip to next"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Skip to previous"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Resize"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App does not support launch on secondary displays."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Split-screen divider"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Left full screen"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Left 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Left 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Left 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Right full screen"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Top full screen"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Top 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Top 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Top 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Bottom full screen"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Using one-handed mode"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"To exit, swipe up from the bottom of the screen or tap anywhere above the app"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Start one-handed mode"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Exit one-handed mode"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Settings for <xliff:g id="APP_NAME">%1$s</xliff:g> bubbles"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Overflow"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Add back to stack"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> from <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> from <xliff:g id="APP_NAME">%2$s</xliff:g> and <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> more"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Move top left"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Move top right"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Move bottom left"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Move bottom right"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> settings"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dismiss bubble"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Don’t bubble conversation"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat using bubbles"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"New conversations appear as floating icons, or bubbles. Tap to open bubble. Drag to move it."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Control bubbles anytime"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Tap Manage to turn off bubbles from this app"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Got it"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"No recent bubbles"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Recent bubbles and dismissed bubbles will appear here"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml new file mode 100644 index 000000000000..3f9ef0ea2816 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-Picture"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(No title program)"</string> + <string name="pip_close" msgid="9135220303720555525">"Close PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml new file mode 100644 index 000000000000..7244b1a1bcf5 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Cerrar"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Expandir"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Configuración"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menú"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está en modo de Pantalla en pantalla"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Si no quieres que <xliff:g id="NAME">%s</xliff:g> use esta función, presiona para abrir la configuración y desactivarla."</string> + <string name="pip_play" msgid="3496151081459417097">"Reproducir"</string> + <string name="pip_pause" msgid="690688849510295232">"Pausar"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Siguiente"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Anterior"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Cambiar el tamaño"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Es posible que la app no funcione en el modo de pantalla dividida."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"La app no es compatible con la función de pantalla dividida."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Es posible que la app no funcione en una pantalla secundaria."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"La app no puede iniciarse en pantallas secundarias."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Divisor de pantalla dividida"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Pantalla izquierda completa"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Izquierda: 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Izquierda: 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Izquierda: 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Pantalla derecha completa"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Pantalla superior completa"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Superior: 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Superior: 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Superior: 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Pantalla inferior completa"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Cómo usar el modo de una mano"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Para salir, desliza el dedo hacia arriba desde la parte inferior de la pantalla o presiona cualquier parte arriba de la app"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Iniciar el modo de una mano"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Salir del modo de una mano"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Configuración para burbujas de <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Menú ampliado"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Volver a agregar a la pila"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g> y <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> más"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Ubicar arriba a la izquierda"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Ubicar arriba a la derecha"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Ubicar abajo a la izquierda"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Ubicar abajo a la derecha"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Configuración de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Descartar burbuja"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"No mostrar la conversación en burbujas"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat con burbujas"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Las conversaciones nuevas aparecen como elementos flotantes o burbujas. Presiona para abrir la burbuja. Arrástrala para moverla."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Controla las burbujas"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Presiona Administrar para desactivar las burbujas de esta app"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Entendido"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"No hay burbujas recientes"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Las burbujas recientes y las que se descartaron aparecerán aquí"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Cuadro"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Administrar"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Se descartó el cuadro."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml new file mode 100644 index 000000000000..5d5954a19761 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pantalla en pantalla"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Sin título de programa)"</string> + <string name="pip_close" msgid="9135220303720555525">"Cerrar PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml new file mode 100644 index 000000000000..65e75bde573d --- /dev/null +++ b/libs/WindowManager/Shell/res/values-es/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Cerrar"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Mostrar"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Ajustes"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menú"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está en imagen en imagen"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Si no quieres que <xliff:g id="NAME">%s</xliff:g> utilice esta función, toca la notificación para abrir los ajustes y desactivarla."</string> + <string name="pip_play" msgid="3496151081459417097">"Reproducir"</string> + <string name="pip_pause" msgid="690688849510295232">"Pausar"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Saltar al siguiente"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Volver al anterior"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Cambiar tamaño"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Es posible que la aplicación no funcione con la pantalla dividida."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"La aplicación no admite la pantalla dividida."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Es posible que la aplicación no funcione en una pantalla secundaria."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"La aplicación no se puede abrir en pantallas secundarias."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Dividir la pantalla"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Pantalla izquierda completa"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Izquierda 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Izquierda 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Izquierda 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Pantalla derecha completa"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Pantalla superior completa"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Superior 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Superior 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Superior 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Pantalla inferior completa"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Utilizar el modo una mano"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Para salir, desliza el dedo hacia arriba desde la parte inferior de la pantalla o toca cualquier zona que haya encima de la aplicación"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Iniciar modo una mano"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Salir del modo una mano"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Ajustes de las burbujas de <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Menú adicional"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Volver a añadir a la pila"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g> y <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> más"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mover arriba a la izquierda"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover arriba a la derecha"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mover abajo a la izquierda."</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover abajo a la derecha"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Ajustes de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Cerrar burbuja"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"No mostrar conversación en burbuja"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatea con burbujas"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Las conversaciones nuevas aparecen como iconos flotantes llamadas \"burbujas\". Toca para abrir la burbuja. Arrastra para moverla."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Controla las burbujas"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Toca Gestionar para desactivar las burbujas de esta aplicación"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Entendido"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"No hay burbujas recientes"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Las burbujas recientes y las cerradas aparecerán aquí"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Burbuja"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestionar"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Burbuja cerrada."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-es/strings_tv.xml b/libs/WindowManager/Shell/res/values-es/strings_tv.xml new file mode 100644 index 000000000000..d31b9b45cae3 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-es/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Imagen en imagen"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa sin título)"</string> + <string name="pip_close" msgid="9135220303720555525">"Cerrar PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-et/strings.xml b/libs/WindowManager/Shell/res/values-et/strings.xml new file mode 100644 index 000000000000..0ccfcfee85d6 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-et/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Sule"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Laiendamine"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Seaded"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menüü"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> on režiimis Pilt pildis"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Kui te ei soovi, et rakendus <xliff:g id="NAME">%s</xliff:g> seda funktsiooni kasutaks, puudutage seadete avamiseks ja lülitage see välja."</string> + <string name="pip_play" msgid="3496151081459417097">"Esita"</string> + <string name="pip_pause" msgid="690688849510295232">"Peata"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Järgmise juurde"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Eelmise juurde"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Suuruse muutmine"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Rakendus ei pruugi poolitatud ekraaniga töötada."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Rakendus ei toeta jagatud ekraani."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Rakendus ei pruugi teisesel ekraanil töötada."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Rakendus ei toeta teisestel ekraanidel käivitamist."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Ekraanijagaja"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Vasak täisekraan"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Vasak: 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Vasak: 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Vasak: 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Parem täisekraan"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Ülemine täisekraan"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Ülemine: 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Ülemine: 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Ülemine: 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Alumine täisekraan"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Ühekäerežiimi kasutamine"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Väljumiseks pühkige ekraani alaosast üles või puudutage rakenduse kohal olevat ala"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Ühekäerežiimi käivitamine"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Ühekäerežiimist väljumine"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Rakenduse <xliff:g id="APP_NAME">%1$s</xliff:g> mullide seaded"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Ületäide"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Lisa tagasi virna"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> rakendusest <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> rakenduselt <xliff:g id="APP_NAME">%2$s</xliff:g> ja veel <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Teisalda üles vasakule"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Teisalda üles paremale"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Teisalda alla vasakule"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Teisalda alla paremale"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Rakenduse <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> seaded"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Sule mull"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ära kuva vestlust mullina"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Vestelge mullide abil"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Uued vestlused kuvatakse hõljuvate ikoonidena ehk mullidena. Puudutage mulli avamiseks. Lohistage mulli, et seda liigutada."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Juhtige mulle igal ajal"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Selle rakenduse puhul mullide väljalülitamiseks puudutage valikut Halda"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Selge"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Hiljutisi mulle pole"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Siin kuvatakse hiljutised ja suletud mullid."</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Mull"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Halda"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Mullist loobuti."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-et/strings_tv.xml b/libs/WindowManager/Shell/res/values-et/strings_tv.xml new file mode 100644 index 000000000000..bc7a6adafc03 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-et/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pilt pildis"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programmi pealkiri puudub)"</string> + <string name="pip_close" msgid="9135220303720555525">"Sule PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Täisekraan"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-eu/strings.xml b/libs/WindowManager/Shell/res/values-eu/strings.xml new file mode 100644 index 000000000000..6682ea80cf42 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-eu/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Itxi"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Zabaldu"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Ezarpenak"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menua"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"Pantaila txiki gainjarrian dago <xliff:g id="NAME">%s</xliff:g>"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Ez baduzu nahi <xliff:g id="NAME">%s</xliff:g> zerbitzuak eginbide hori erabiltzea, sakatu hau ezarpenak ireki eta aukera desaktibatzeko."</string> + <string name="pip_play" msgid="3496151081459417097">"Erreproduzitu"</string> + <string name="pip_pause" msgid="690688849510295232">"Pausatu"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Saltatu hurrengora"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Saltatu aurrekora"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Aldatu tamaina"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Baliteke aplikazioak ez funtzionatzea pantaila zatituan."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikazioak ez du onartzen pantaila zatitua"</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Baliteke aplikazioak ez funtzionatzea bigarren mailako pantailetan."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikazioa ezin da abiarazi bigarren mailako pantailatan."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Pantaila-zatitzailea"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ezarri ezkerraldea pantaila osoan"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Ezarri ezkerraldea % 70en"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Ezarri ezkerraldea % 50en"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Ezarri ezkerraldea % 30en"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Ezarri eskuinaldea pantaila osoan"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Ezarri goialdea pantaila osoan"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Ezarri goialdea % 70en"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Ezarri goialdea % 50en"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Ezarri goialdea % 30en"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Ezarri behealdea pantaila osoan"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Esku bakarreko modua erabiltzea"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Irteteko, pasatu hatza pantailaren behealdetik gora edo sakatu aplikazioaren gainaldea"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Abiarazi esku bakarreko modua"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Irten esku bakarreko modutik"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> aplikazioaren ezarpenen burbuilak"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Gainezkatzea"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Gehitu berriro errenkadan"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> (<xliff:g id="APP_NAME">%2$s</xliff:g>)"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> aplikazioaren \"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>\" jakinarazpena, eta beste <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Eraman goialdera, ezkerretara"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Eraman goialdera, eskuinetara"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Eraman behealdera, ezkerretara"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Eraman behealdera, eskuinetara"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> aplikazioaren ezarpenak"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Baztertu burbuila"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ez erakutsi elkarrizketak burbuila gisa"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Txateatu burbuilen bidez"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Elkarrizketa berriak ikono gainerakor edo burbuila gisa agertzen dira. Sakatu burbuila irekitzeko. Arrasta ezazu mugitzeko."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Kontrolatu burbuilak edonoiz"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Aplikazioaren burbuilak desaktibatzeko, sakatu Kudeatu"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Ados"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Ez dago azkenaldiko burbuilarik"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Azken burbuilak eta baztertutakoak agertuko dira hemen"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Burbuila"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Kudeatu"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Baztertu da globoa."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-eu/strings_tv.xml b/libs/WindowManager/Shell/res/values-eu/strings_tv.xml new file mode 100644 index 000000000000..cf5f98883082 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-eu/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pantaila txiki gainjarria"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa izengabea)"</string> + <string name="pip_close" msgid="9135220303720555525">"Itxi PIPa"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Pantaila osoa"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml new file mode 100644 index 000000000000..a41811d53357 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-fa/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"بستن"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"بزرگ کردن"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"تنظیمات"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"منو"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> درحالت تصویر در تصویر است"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"اگر نمیخواهید <xliff:g id="NAME">%s</xliff:g> از این قابلیت استفاده کند، با ضربه زدن، تنظیمات را باز کنید و آن را خاموش کنید."</string> + <string name="pip_play" msgid="3496151081459417097">"پخش"</string> + <string name="pip_pause" msgid="690688849510295232">"توقف موقت"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"رد شدن به بعدی"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"رد شدن به قبلی"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"تغییر اندازه"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"ممکن است برنامه با «صفحهٔ دونیمه» کار نکند."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"برنامه از تقسیم صفحه پشتیبانی نمیکند."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ممکن است برنامه در نمایشگر ثانویه کار نکند."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"برنامه از راهاندازی در نمایشگرهای ثانویه پشتیبانی نمیکند."</string> + <string name="accessibility_divider" msgid="703810061635792791">"تقسیمکننده صفحه"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"تمامصفحه چپ"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"٪۷۰ چپ"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"٪۵۰ چپ"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"٪۳۰ چپ"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"تمامصفحه راست"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"تمامصفحه بالا"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"٪۷۰ بالا"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"٪۵۰ بالا"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"٪۳۰ بالا"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"تمامصفحه پایین"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"استفاده از «حالت تک حرکت»"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"برای خارج شدن، از پایین صفحهنمایش تند بهطرف بالا بکشید یا در هر جایی از بالای برنامه که میخواهید ضربه بزنید"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"آغاز «حالت تک حرکت»"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"خروج از «حالت تک حرکت»"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"تنظیمات برای حبابکهای <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"لبریزشده"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"افزودن برگشت به پشته"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> از <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> از <xliff:g id="APP_NAME">%2$s</xliff:g> و <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> مورد بیشتر"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"انتقال به بالا سمت راست"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"انتقال به بالا سمت چپ"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"انتقال به پایین سمت راست"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"انتقال به پایین سمت چپ"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"تنظیمات <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"رد کردن حبابک"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"مکالمه در حباب نشان داده نشود"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"گپ بااستفاده از حبابکها"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"مکالمههای جدید بهصورت نمادهای شناور یا حبابکها نشان داده میشوند. برای باز کردن حبابکها ضربه بزنید. برای جابهجایی، آن را بکشید."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"کنترل حبابکها در هرزمانی"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"برای خاموش کردن «حبابکها» از این برنامه، روی «مدیریت» ضربه بزنید"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"متوجهام"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"هیچ حبابک جدیدی وجود ندارد"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"حبابکها اخیر و حبابکها ردشده اینجا ظاهر خواهند شد"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"حباب"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"مدیریت"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"حبابک رد شد."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-fa/strings_tv.xml b/libs/WindowManager/Shell/res/values-fa/strings_tv.xml new file mode 100644 index 000000000000..5b815b4c7b86 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-fa/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"تصویر در تصویر"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(برنامه بدون عنوان)"</string> + <string name="pip_close" msgid="9135220303720555525">"بستن PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"تمام صفحه"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-fi/strings.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml new file mode 100644 index 000000000000..fcdc70fc9cda --- /dev/null +++ b/libs/WindowManager/Shell/res/values-fi/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Sulje"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Laajenna"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Asetukset"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Valikko"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> on kuva kuvassa ‑tilassa"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Jos et halua, että <xliff:g id="NAME">%s</xliff:g> voi käyttää tätä ominaisuutta, avaa asetukset napauttamalla ja poista se käytöstä."</string> + <string name="pip_play" msgid="3496151081459417097">"Toista"</string> + <string name="pip_pause" msgid="690688849510295232">"Keskeytä"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Siirry seuraavaan"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Siirry edelliseen"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Muuta kokoa"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Sovellus ei ehkä toimi jaetulla näytöllä."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Sovellus ei tue jaetun näytön tilaa."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Sovellus ei ehkä toimi toissijaisella näytöllä."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Sovellus ei tue käynnistämistä toissijaisilla näytöillä."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Näytön jakaja"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Vasen koko näytölle"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Vasen 70 %"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Vasen 50 %"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Vasen 30 %"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Oikea koko näytölle"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Yläosa koko näytölle"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Yläosa 70 %"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Yläosa 50 %"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Yläosa 30 %"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Alaosa koko näytölle"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Yhden käden moodin käyttö"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Poistu pyyhkäisemällä ylös näytön alareunasta tai napauttamalla sovelluksen yllä"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Käynnistä yhden käden moodi"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Poistu yhden käden moodista"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Kuplien asetukset: <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Ylivuoto"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Lisää takaisin pinoon"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g>: <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> (<xliff:g id="APP_NAME">%2$s</xliff:g>) ja <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> muuta"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Siirrä vasempaan yläreunaan"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Siirrä oikeaan yläreunaan"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Siirrä vasempaan alareunaan"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Siirrä oikeaan alareunaan"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>: asetukset"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ohita kupla"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Älä näytä kuplia keskusteluista"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chattaile kuplien avulla"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Uudet keskustelut näkyvät kelluvina kuvakkeina tai kuplina. Avaa kupla napauttamalla. Siirrä sitä vetämällä."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Muuta kuplien asetuksia milloin tahansa"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Valitse Ylläpidä, jos haluat poistaa kuplat käytöstä tästä sovelluksesta"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Okei"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Ei viimeaikaisia kuplia"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Viimeaikaiset ja äskettäin ohitetut kuplat näkyvät täällä"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Kupla"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Ylläpidä"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Kupla ohitettu."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-fi/strings_tv.xml b/libs/WindowManager/Shell/res/values-fi/strings_tv.xml new file mode 100644 index 000000000000..77ad6eef91e7 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-fi/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Kuva kuvassa"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Nimetön)"</string> + <string name="pip_close" msgid="9135220303720555525">"Sulje PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Koko näyttö"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml new file mode 100644 index 000000000000..ed822373e557 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Fermer"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Développer"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Paramètres"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> est en mode d\'incrustation d\'image"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Si vous ne voulez pas que <xliff:g id="NAME">%s</xliff:g> utilise cette fonctionnalité, touchez l\'écran pour ouvrir les paramètres, puis désactivez-la."</string> + <string name="pip_play" msgid="3496151081459417097">"Lire"</string> + <string name="pip_pause" msgid="690688849510295232">"Interrompre"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Passer au suivant"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Revenir au précédent"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionner"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Il est possible que l\'application ne fonctionne pas en mode Écran partagé."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"L\'application n\'est pas compatible avec l\'écran partagé."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Il est possible que l\'application ne fonctionne pas sur un écran secondaire."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"L\'application ne peut pas être lancée sur des écrans secondaires."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Séparateur d\'écran partagé"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Plein écran à la gauche"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70 % à la gauche"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50 % à la gauche"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"30 % à la gauche"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Plein écran à la droite"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Plein écran dans le haut"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"70 % dans le haut"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50 % dans le haut"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"30 % dans le haut"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Plein écran dans le bas"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Utiliser le mode Une main"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Pour quitter, balayez l\'écran du bas vers le haut, ou touchez n\'importe où sur l\'écran en haut de l\'application"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Démarrer le mode Une main"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Quitter le mode Une main"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Paramètres pour les bulles de l\'application <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Menu déroulant"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Replacer sur la pile"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g> et <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> autres"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Déplacer dans coin sup. gauche"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Déplacer dans coin sup. droit"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Déplacer dans coin inf. gauche"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Déplacer dans coin inf. droit"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Paramètres <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ignorer la bulle"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ne pas afficher les conversations dans des bulles"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Clavarder en utilisant des bulles"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Les nouvelles conversations s\'affichent sous forme d\'icônes flottantes (de bulles). Touchez une bulle pour l\'ouvrir. Faites-la glisser pour la déplacer."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Paramètres des bulles"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Toucher Gérer pour désactiver les bulles de cette application"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Aucune bulle récente"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Les bulles récentes et les bulles ignorées s\'afficheront ici"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Bulle"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Gérer"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bulle ignorée."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml new file mode 100644 index 000000000000..0ec7f40f0e9f --- /dev/null +++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Incrustation d\'image"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Aucun programme de titre)"</string> + <string name="pip_close" msgid="9135220303720555525">"Fermer mode IDI"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Plein écran"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml new file mode 100644 index 000000000000..ad98b85d5d5d --- /dev/null +++ b/libs/WindowManager/Shell/res/values-fr/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Fermer"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Développer"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Paramètres"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> est en mode Picture-in-picture"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Si vous ne voulez pas que l\'application <xliff:g id="NAME">%s</xliff:g> utilise cette fonctionnalité, appuyez ici pour ouvrir les paramètres et la désactiver."</string> + <string name="pip_play" msgid="3496151081459417097">"Lecture"</string> + <string name="pip_pause" msgid="690688849510295232">"Suspendre"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Passer au contenu suivant"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Passer au contenu précédent"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionner"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Il est possible que l\'application ne fonctionne pas en mode Écran partagé."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Application incompatible avec l\'écran partagé."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Il est possible que l\'application ne fonctionne pas sur un écran secondaire."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"L\'application ne peut pas être lancée sur des écrans secondaires."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Séparateur d\'écran partagé"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Écran de gauche en plein écran"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Écran de gauche à 70 %"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Écran de gauche à 50 %"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Écran de gauche à 30 %"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Écran de droite en plein écran"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Écran du haut en plein écran"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Écran du haut à 70 %"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Écran du haut à 50 %"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Écran du haut à 30 %"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Écran du bas en plein écran"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Utiliser le mode une main"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Pour quitter, balayez l\'écran de bas en haut ou appuyez n\'importe où au-dessus de l\'application"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Démarrer le mode une main"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Quitter le mode une main"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Paramètres des bulles de <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Dépassement"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Ajouter à nouveau l\'élément à la pile"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de l\'application <xliff:g id="APP_NAME">%2$s</xliff:g> et <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> autres"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Déplacer en haut à gauche"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Déplacer en haut à droite"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Déplacer en bas à gauche"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Déplacer en bas à droite"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Paramètres <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Fermer la bulle"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ne pas afficher la conversation dans une bulle"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatter en utilisant des bulles"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Les nouvelles conversations s\'affichent sous forme d\'icônes flottantes ou bulles. Appuyez sur la bulle pour l\'ouvrir. Faites-la glisser pour la déplacer."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Contrôler les paramètres des bulles"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Appuyez sur \"Gérer\" pour désactiver les bulles de cette application"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Aucune bulle récente"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Les bulles récentes et ignorées s\'afficheront ici"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Bulle"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Gérer"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bulle fermée."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-fr/strings_tv.xml b/libs/WindowManager/Shell/res/values-fr/strings_tv.xml new file mode 100644 index 000000000000..27fd155535b7 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-fr/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programme sans titre)"</string> + <string name="pip_close" msgid="9135220303720555525">"Fermer mode PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Plein écran"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml new file mode 100644 index 000000000000..529825e68151 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-gl/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Pechar"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Despregar"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Configuración"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menú"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está na pantalla superposta"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Se non queres que <xliff:g id="NAME">%s</xliff:g> utilice esta función, toca a configuración para abrir as opcións e desactivar a función."</string> + <string name="pip_play" msgid="3496151081459417097">"Reproducir"</string> + <string name="pip_pause" msgid="690688849510295232">"Pausar"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Ir ao seguinte"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Ir ao anterior"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Cambiar tamaño"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Pode que a aplicación non funcione coa pantalla dividida."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"A aplicación non é compatible coa función de pantalla dividida."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"É posible que a aplicación non funcione nunha pantalla secundaria."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"A aplicación non se pode iniciar en pantallas secundarias."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Divisor de pantalla dividida"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Pantalla completa á esquerda"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70 % á esquerda"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50 % á esquerda"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"30 % á esquerda"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Pantalla completa á dereita"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Pantalla completa arriba"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"70 % arriba"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50 % arriba"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"30 % arriba"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Pantalla completa abaixo"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Como se usa o modo dunha soa man?"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Para saír, pasa o dedo cara arriba desde a parte inferior da pantalla ou toca calquera lugar da zona situada encima da aplicación"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Iniciar modo dunha soa man"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Saír do modo dunha soa man"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Configuración das burbullas de <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Mostrar menú adicional"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Engadir de novo á pilla"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g> e <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> máis"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mover á parte super. esquerda"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover á parte superior dereita"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mover á parte infer. esquerda"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover á parte inferior dereita"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Configuración de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ignorar burbulla"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Non mostrar a conversa como burbulla"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatear usando burbullas"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"As conversas novas aparecen como iconas flotantes ou burbullas. Toca para abrir a burbulla e arrastra para movela."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Controla as burbullas"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Para desactivar as burbullas nesta aplicación, toca Xestionar"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Entendido"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Non hai burbullas recentes"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"As burbullas recentes e ignoradas aparecerán aquí."</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Burbulla"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Xestionar"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Ignorouse a burbulla."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-gl/strings_tv.xml b/libs/WindowManager/Shell/res/values-gl/strings_tv.xml new file mode 100644 index 000000000000..df96f6cb794d --- /dev/null +++ b/libs/WindowManager/Shell/res/values-gl/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pantalla superposta"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa sen título)"</string> + <string name="pip_close" msgid="9135220303720555525">"Pechar PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Pantalla completa"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-gu/strings.xml b/libs/WindowManager/Shell/res/values-gu/strings.xml new file mode 100644 index 000000000000..ee23e1e967ec --- /dev/null +++ b/libs/WindowManager/Shell/res/values-gu/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"બંધ કરો"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"વિસ્તૃત કરો"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"સેટિંગ"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"મેનૂ"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ચિત્રમાં-ચિત્રની અંદર છે"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"જો તમે નથી ઇચ્છતા કે <xliff:g id="NAME">%s</xliff:g> આ સુવિધાનો ઉપયોગ કરે, તો સેટિંગ ખોલવા માટે ટૅપ કરો અને તેને બંધ કરો."</string> + <string name="pip_play" msgid="3496151081459417097">"ચલાવો"</string> + <string name="pip_pause" msgid="690688849510295232">"થોભાવો"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"આગલા પર જાઓ"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"પહેલાંના પર જાઓ"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"કદ બદલો"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"વિભાજિત-સ્ક્રીન સાથે ઍપ કદાચ કામ ન કરે."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ઍપ્લિકેશન સ્ક્રીન-વિભાજનનું સમર્થન કરતી નથી."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ઍપ્લિકેશન ગૌણ ડિસ્પ્લે પર કદાચ કામ ન કરે."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ઍપ્લિકેશન ગૌણ ડિસ્પ્લે પર લૉન્ચનું સમર્થન કરતી નથી."</string> + <string name="accessibility_divider" msgid="703810061635792791">"સ્પ્લિટ-સ્ક્રીન વિભાજક"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ડાબી પૂર્ણ સ્ક્રીન"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ડાબે 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ડાબે 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ડાબે 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"જમણી સ્ક્રીન સ્ક્રીન"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"શીર્ષ પૂર્ણ સ્ક્રીન"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"શીર્ષ 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"શીર્ષ 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"શીર્ષ 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"તળિયાની પૂર્ણ સ્ક્રીન"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"એક-હાથે વાપરો મોડનો ઉપયોગ કરી રહ્યાં છીએ"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"બહાર નીકળવા માટે, સ્ક્રીનની નીચેના ભાગથી ઉપરની તરફ સ્વાઇપ કરો અથવા ઍપના આઇકન પર ગમે ત્યાં ટૅપ કરો"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"એક-હાથે વાપરો મોડ શરૂ કરો"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"એક-હાથે વાપરો મોડમાંથી બહાર નીકળો"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> બબલ માટેનાં સેટિંગ"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"ઓવરફ્લો"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"સ્ટૅકમાં ફરી ઉમેરો"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g> તરફથી <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> અને વધુ <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> તરફથી <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"ઉપર ડાબે ખસેડો"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ઉપર જમણે ખસેડો"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"નીચે ડાબે ખસેડો"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"નીચે જમણે ખસેડો"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> સેટિંગ"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"બબલને છોડી દો"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"વાતચીતને બબલ કરશો નહીં"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"બબલનો ઉપયોગ કરીને ચેટ કરો"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"નવી વાતચીત ફ્લોટિંગ આઇકન અથવા બબલ જેવી દેખાશે. બબલને ખોલવા માટે ટૅપ કરો. તેને ખસેડવા માટે ખેંચો."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"બબલને કોઈપણ સમયે નિયંત્રિત કરો"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"આ ઍપમાંથી બબલને બંધ કરવા માટે મેનેજ કરો પર ટૅપ કરો"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"સમજાઈ ગયું"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"તાજેતરના કોઈ બબલ નથી"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"એકદમ નવા બબલ અને છોડી દીધેલા બબલ અહીં દેખાશે"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"બબલ"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"મેનેજ કરો"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"બબલ છોડી દેવાયો."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-gu/strings_tv.xml b/libs/WindowManager/Shell/res/values-gu/strings_tv.xml new file mode 100644 index 000000000000..3608f1d530c0 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-gu/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ચિત્રમાં-ચિત્ર"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(કોઈ ટાઇટલ પ્રોગ્રામ નથી)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP બંધ કરો"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"પૂર્ણ સ્ક્રીન"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml new file mode 100644 index 000000000000..34c1c85211f6 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-hi/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"बंद करें"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"विस्तार करें"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"सेटिंग"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"मेन्यू"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> \"पिक्चर में पिक्चर\" के अंदर है"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"अगर आप नहीं चाहते कि <xliff:g id="NAME">%s</xliff:g> इस सुविधा का उपयोग करे, तो सेटिंग खोलने के लिए टैप करें और उसे बंद करें ."</string> + <string name="pip_play" msgid="3496151081459417097">"चलाएं"</string> + <string name="pip_pause" msgid="690688849510295232">"रोकें"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"अगले पर जाएं"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"पिछले पर जाएं"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"आकार बदलें"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"ऐप्लिकेशन शायद स्प्लिट स्क्रीन मोड में काम न करे."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ऐप विभाजित स्क्रीन का समर्थन नहीं करता है."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"हो सकता है कि ऐप प्राइमरी (मुख्य) डिस्प्ले के अलावा बाकी दूसरे डिस्प्ले पर काम न करे."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"प्राइमरी (मुख्य) डिस्प्ले के अलावा बाकी दूसरे डिस्प्ले पर ऐप लॉन्च नहीं किया जा सकता."</string> + <string name="accessibility_divider" msgid="703810061635792791">"विभाजित स्क्रीन विभाजक"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"बाईं स्क्रीन को फ़ुल स्क्रीन बनाएं"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"बाईं स्क्रीन को 70% बनाएं"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"बाईं स्क्रीन को 50% बनाएं"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"बाईं स्क्रीन को 30% बनाएं"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"दाईं स्क्रीन को फ़ुल स्क्रीन बनाएं"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"ऊपर की स्क्रीन को फ़ुल स्क्रीन बनाएं"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ऊपर की स्क्रीन को 70% बनाएं"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ऊपर की स्क्रीन को 50% बनाएं"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ऊपर की स्क्रीन को 30% बनाएं"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"नीचे की स्क्रीन को फ़ुल स्क्रीन बनाएं"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"वन-हैंडेड मोड का इस्तेमाल करना"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"इस मोड से बाहर निकलने के लिए, स्क्रीन के सबसे निचले हिस्से से ऊपर की ओर स्वाइप करें या ऐप्लिकेशन के बाहर कहीं भी टैप करें"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"वन-हैंडेड मोड चालू करें"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"वन-हैंडेड मोड से बाहर निकलें"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> बबल्स की सेटिंग"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"ओवरफ़्लो"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"स्टैक में वापस जोड़ें"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g> से <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> और <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> अन्य ऐप्लिकेशन से <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"सबसे ऊपर बाईं ओर ले जाएं"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"सबसे ऊपर दाईं ओर ले जाएं"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"बाईं ओर सबसे नीचे ले जाएं"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"सबसे नीचे दाईं ओर ले जाएं"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> की सेटिंग"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"बबल खारिज करें"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"बातचीत को बबल न करें"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"बबल्स का इस्तेमाल करके चैट करें"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"नई बातचीत फ़्लोटिंग आइकॉन या बबल्स की तरह दिखेंगी. बबल को खोलने के लिए टैप करें. इसे एक जगह से दूसरी जगह ले जाने के लिए खींचें और छोड़ें."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"जब चाहें, बबल्स को कंट्रोल करें"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"इस ऐप्लिकेशन पर बबल्स को बंद करने के लिए \'प्रबंधित करें\' पर टैप करें"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"ठीक है"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"हाल ही के बबल्स मौजूद नहीं हैं"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"हाल ही के बबल्स और हटाए गए बबल्स यहां दिखेंगे"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"बबल"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"प्रबंधित करें"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"बबल खारिज किया गया."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-hi/strings_tv.xml b/libs/WindowManager/Shell/res/values-hi/strings_tv.xml new file mode 100644 index 000000000000..720bb6ca5e24 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-hi/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"पिक्चर में पिक्चर"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(कोई शीर्षक कार्यक्रम नहीं)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP बंद करें"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"फ़ुल स्क्रीन"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-hr/strings.xml b/libs/WindowManager/Shell/res/values-hr/strings.xml new file mode 100644 index 000000000000..32b21aadbb2f --- /dev/null +++ b/libs/WindowManager/Shell/res/values-hr/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Zatvori"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Proširivanje"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Postavke"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Izbornik"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> jest na slici u slici"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Ako ne želite da aplikacija <xliff:g id="NAME">%s</xliff:g> upotrebljava tu značajku, dodirnite da biste otvorili postavke i isključili je."</string> + <string name="pip_play" msgid="3496151081459417097">"Reproduciraj"</string> + <string name="pip_pause" msgid="690688849510295232">"Pauziraj"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Preskoči na sljedeće"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Preskoči na prethodno"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Promjena veličine"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacija možda neće funkcionirati s podijeljenim zaslonom."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podržava podijeljeni zaslon."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija možda neće funkcionirati na sekundarnom zaslonu."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacija ne podržava pokretanje na sekundarnim zaslonima."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Razdjelnik podijeljenog zaslona"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Lijevi zaslon u cijeli zaslon"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Lijevi zaslon na 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Lijevi zaslon na 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Lijevi zaslon na 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Desni zaslon u cijeli zaslon"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Gornji zaslon u cijeli zaslon"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Gornji zaslon na 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Gornji zaslon na 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Gornji zaslon na 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Donji zaslon u cijeli zaslon"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Korištenje načina rada jednom rukom"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Za izlaz prijeđite prstom od dna zaslona prema gore ili dodirnite bio gdje iznad aplikacije"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Pokretanje načina rada jednom rukom"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Izlaz iz načina rada jednom rukom"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Postavke za oblačiće za aplikaciju <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Dodatno"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Dodajte natrag u nizove"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> iz aplikacije <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> iz aplikacije <xliff:g id="APP_NAME">%2$s</xliff:g> i još <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Premjesti u gornji lijevi kut"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Premjesti u gornji desni kut"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Premjesti u donji lijevi kut"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Premjestite u donji desni kut"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Postavke za <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Odbaci oblačić"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Zaustavi razgovor u oblačićima"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Oblačići u chatu"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Novi razgovori pojavljuju se kao pomične ikone ili oblačići. Dodirnite za otvaranje oblačića. Povucite da biste ga premjestili."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Upravljanje oblačićima u svakom trenutku"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Dodirnite Upravljanje da biste isključili oblačiće iz ove aplikacije"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Shvaćam"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Nema nedavnih oblačića"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Ovdje će se prikazivati nedavni i odbačeni oblačići"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Oblačić"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljanje"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblačić odbačen."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-hr/strings_tv.xml b/libs/WindowManager/Shell/res/values-hr/strings_tv.xml new file mode 100644 index 000000000000..21f8cb63f470 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-hr/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Slika u slici"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez naslova)"</string> + <string name="pip_close" msgid="9135220303720555525">"Zatvori PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Cijeli zaslon"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-hu/strings.xml b/libs/WindowManager/Shell/res/values-hu/strings.xml new file mode 100644 index 000000000000..123b127bd5a3 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-hu/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Bezárás"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Kibontás"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Beállítások"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menü"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"A(z) <xliff:g id="NAME">%s</xliff:g> kép a képben funkciót használ"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Ha nem szeretné, hogy a(z) <xliff:g id="NAME">%s</xliff:g> használja ezt a funkciót, koppintson a beállítások megnyitásához, és kapcsolja ki."</string> + <string name="pip_play" msgid="3496151081459417097">"Lejátszás"</string> + <string name="pip_pause" msgid="690688849510295232">"Szüneteltetés"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Ugrás a következőre"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Ugrás az előzőre"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Átméretezés"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Lehet, hogy az alkalmazás nem működik osztott képernyős nézetben."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Az alkalmazás nem támogatja az osztott képernyős nézetet."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Előfordulhat, hogy az alkalmazás nem működik másodlagos kijelzőn."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Az alkalmazást nem lehet másodlagos kijelzőn elindítani."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Elválasztó az osztott nézetben"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Bal oldali teljes képernyőre"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Bal oldali 70%-ra"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Bal oldali 50%-ra"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Bal oldali 30%-ra"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Jobb oldali teljes képernyőre"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Felső teljes képernyőre"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Felső 70%-ra"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Felső 50%-ra"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Felső 30%-ra"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Alsó teljes képernyőre"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Egykezes mód használata"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"A kilépéshez csúsztasson felfelé a képernyő aljáról, vagy koppintson az alkalmazás felett a képernyő bármelyik részére"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Egykezes mód indítása"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Kilépés az egykezes módból"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"A(z) <xliff:g id="APP_NAME">%1$s</xliff:g>-buborékok beállításai"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"További elemeket tartalmazó menü"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Visszaküldés a verembe"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>, <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> a(z) <xliff:g id="APP_NAME">%2$s</xliff:g> alkalmazásból és <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> további"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Áthelyezés fel és balra"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Áthelyezés fel és jobbra"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Áthelyezés le és balra"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Áthelyezés le és jobbra"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> beállításai"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Buborék elvetése"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ne jelenjen meg a beszélgetés buborékban"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Buborékokat használó csevegés"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Az új beszélgetések lebegő ikonként, vagyis buborékként jelennek meg. A buborék megnyitásához koppintson rá. Áthelyezéshez húzza a kívánt helyre."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Buborékok vezérlése bármikor"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"A Kezelés gombra koppintva kapcsolhatja ki az alkalmazásból származó buborékokat"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Értem"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Nincsenek buborékok a közelmúltból"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"A legutóbbi és az elvetett buborékok itt jelennek majd meg"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Buborék"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Kezelés"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Buborék elvetve."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-hu/strings_tv.xml b/libs/WindowManager/Shell/res/values-hu/strings_tv.xml new file mode 100644 index 000000000000..0010086bb0b5 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-hu/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Kép a képben"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Cím nélküli program)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP bezárása"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Teljes képernyő"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-hy/strings.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml new file mode 100644 index 000000000000..b047cf131aa8 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-hy/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Փակել"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Ընդարձակել"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Կարգավորումներ"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Ընտրացանկ"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>-ը «Նկար նկարի մեջ» ռեժիմում է"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Եթե չեք ցանկանում, որ <xliff:g id="NAME">%s</xliff:g>-ն օգտագործի այս գործառույթը, հպեք՝ կարգավորումները բացելու և այն անջատելու համար։"</string> + <string name="pip_play" msgid="3496151081459417097">"Նվագարկել"</string> + <string name="pip_pause" msgid="690688849510295232">"Դադարեցնել"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Անցնել հաջորդին"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Վերադառնալ նախորդին"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Փոխել չափը"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Հավելվածը չի կարող աշխատել տրոհված էկրանի ռեժիմում։"</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Հավելվածը չի աջակցում էկրանի տրոհումը:"</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Հավելվածը կարող է չաշխատել լրացուցիչ էկրանի վրա"</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Հավելվածը չի աջակցում գործարկումը լրացուցիչ էկրանների վրա"</string> + <string name="accessibility_divider" msgid="703810061635792791">"Տրոհված էկրանի բաժանիչ"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ձախ էկրանը՝ լիաէկրան"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Ձախ էկրանը՝ 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Ձախ էկրանը՝ 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Ձախ էկրանը՝ 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Աջ էկրանը՝ լիաէկրան"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Վերևի էկրանը՝ լիաէկրան"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Վերևի էկրանը՝ 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Վերևի էկրանը՝ 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Վերևի էկրանը՝ 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Ներքևի էկրանը՝ լիաէկրան"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Ինչպես օգտվել մեկ ձեռքի ռեժիմից"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Դուրս գալու համար մատը սահեցրեք էկրանի ներքևից վերև կամ հպեք հավելվածի վերևում որևէ տեղ։"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Գործարկել մեկ ձեռքի ռեժիմը"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Դուրս գալ մեկ ձեռքի ռեժիմից"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g>-ի ամպիկների կարգավորումներ"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Լրացուցիչ ընտրացանկ"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Նորից ավելացնել զտիչներում"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>՝ <xliff:g id="APP_NAME">%2$s</xliff:g>-ից"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>` <xliff:g id="APP_NAME">%2$s</xliff:g>-ից ու ևս <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> ամպիկ"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Տեղափոխել վերև՝ ձախ"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Տեղափոխել վերև՝ աջ"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Տեղափոխել ներքև՝ ձախ"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Տեղափոխել ներքև՝ աջ"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> – կարգավորումներ"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Փակել ամպիկը"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Զրույցը չցուցադրել ամպիկի տեսքով"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Զրույցի ամպիկներ"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Նոր զրույցները կհայտնվեն լողացող պատկերակների կամ ամպիկների տեսքով։ Հպեք՝ ամպիկը բացելու համար։ Քաշեք՝ այն տեղափոխելու համար։"</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Ամպիկների կարգավորումներ"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Հպեք «Կառավարել» կոճակին՝ այս հավելվածի ամպիկներն անջատելու համար։"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Եղավ"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Ամպիկներ չկան"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Այստեղ կցուցադրվեն վերջերս օգտագործված և փակված ամպիկները, որոնք կկարողանաք հեշտությամբ վերաբացել"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Պղպջակ"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Կառավարել"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Ամպիկը փակվեց։"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-hy/strings_tv.xml b/libs/WindowManager/Shell/res/values-hy/strings_tv.xml new file mode 100644 index 000000000000..cb18762be48b --- /dev/null +++ b/libs/WindowManager/Shell/res/values-hy/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Նկար նկարի մեջ"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Առանց վերնագրի ծրագիր)"</string> + <string name="pip_close" msgid="9135220303720555525">"Փակել PIP-ն"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Լիէկրան"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-in/strings.xml b/libs/WindowManager/Shell/res/values-in/strings.xml new file mode 100644 index 000000000000..a75cdb4b2b85 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-in/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Tutup"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Luaskan"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Setelan"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> adalah picture-in-picture"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Jika Anda tidak ingin <xliff:g id="NAME">%s</xliff:g> menggunakan fitur ini, ketuk untuk membuka setelan dan menonaktifkannya."</string> + <string name="pip_play" msgid="3496151081459417097">"Putar"</string> + <string name="pip_pause" msgid="690688849510295232">"Jeda"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Lewati ke berikutnya"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Lewati ke sebelumnya"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ubah ukuran"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikasi mungkin tidak berfungsi dengan layar terpisah."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App tidak mendukung layar terpisah."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikasi mungkin tidak berfungsi pada layar sekunder."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikasi tidak mendukung peluncuran pada layar sekunder."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Pembagi layar terpisah"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Layar penuh di kiri"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Kiri 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Kiri 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Kiri 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Layar penuh di kanan"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Layar penuh di atas"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Atas 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Atas 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Atas 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Layar penuh di bawah"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Menggunakan mode satu tangan"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Untuk keluar, geser layar dari bawah ke atas atau ketuk di mana saja di atas aplikasi"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Mulai mode satu tangan"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Keluar dari mode satu tangan"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Setelan untuk balon <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Tambahan"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Tambahkan kembali ke stack"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> dari <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> dari <xliff:g id="APP_NAME">%2$s</xliff:g> dan <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> lainnya"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Pindahkan ke kiri atas"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Pindahkan ke kanan atas"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Pindahkan ke kiri bawah"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Pindahkan ke kanan bawah"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Setelan <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Tutup balon"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Jangan gunakan percakapan balon"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat dalam tampilan balon"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Percakapan baru muncul sebagai ikon mengambang, atau balon. Ketuk untuk membuka balon. Tarik untuk memindahkannya."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Kontrol balon kapan saja"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Ketuk Kelola untuk menonaktifkan balon dari aplikasi ini"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Oke"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Tidak ada balon baru-baru ini"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Balon yang baru dipakai dan balon yang telah ditutup akan muncul di sini"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Balon"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Kelola"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balon ditutup."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-in/strings_tv.xml b/libs/WindowManager/Shell/res/values-in/strings_tv.xml new file mode 100644 index 000000000000..8f3a28764b00 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-in/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-Picture"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program tanpa judul)"</string> + <string name="pip_close" msgid="9135220303720555525">"Tutup PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Layar penuh"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-is/strings.xml b/libs/WindowManager/Shell/res/values-is/strings.xml new file mode 100644 index 000000000000..3b28148e3171 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-is/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Loka"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Stækka"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Stillingar"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Valmynd"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> er með mynd í mynd"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Ef þú vilt ekki að <xliff:g id="NAME">%s</xliff:g> noti þennan eiginleika skaltu ýta til að opna stillingarnar og slökkva á því."</string> + <string name="pip_play" msgid="3496151081459417097">"Spila"</string> + <string name="pip_pause" msgid="690688849510295232">"Gera hlé"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Fara á næsta"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Fara á fyrra"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Breyta stærð"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Hugsanlega virkar forritið ekki með skjáskiptingu."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Forritið styður ekki að skjánum sé skipt."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Hugsanlegt er að forritið virki ekki á öðrum skjá."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Forrit styður ekki opnun á öðrum skjá."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Skjáskipting"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Vinstri á öllum skjánum"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Vinstri 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Vinstri 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Vinstri 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Hægri á öllum skjánum"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Efri á öllum skjánum"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Efri 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Efri 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Efri 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Neðri á öllum skjánum"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Notkun einhentrar stillingar"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Til að loka skaltu strjúka upp frá neðri hluta skjásins eða ýta hvar sem er fyrir ofan forritið"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Ræsa einhenta stillingu"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Hætta í einhentri stillingu"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Stillingar fyrir blöðrur frá <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Yfirflæði"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Bæta aftur í stafla"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> frá <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"„<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>“ frá <xliff:g id="APP_NAME">%2$s</xliff:g> og <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> í viðbót"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Færa efst til vinstri"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Færa efst til hægri"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Færa neðst til vinstri"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Færðu neðst til hægri"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Stillingar <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Loka blöðru"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ekki setja samtal í blöðru"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Spjalla með blöðrum"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Ný samtöl birtast sem fljótandi tákn eða blöðrur. Ýttu til að opna blöðru. Dragðu hana til að færa."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Hægt er að stjórna blöðrum hvenær sem er"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Ýttu á „Stjórna“ til að slökkva á blöðrum frá þessu forriti"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Ég skil"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Engar nýlegar blöðrur"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Nýlegar blöðrur og blöðrur sem þú hefur lokað birtast hér"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Blaðra"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Stjórna"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Blöðru lokað."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-is/strings_tv.xml b/libs/WindowManager/Shell/res/values-is/strings_tv.xml new file mode 100644 index 000000000000..1f148d948a0e --- /dev/null +++ b/libs/WindowManager/Shell/res/values-is/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Mynd í mynd"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Efni án titils)"</string> + <string name="pip_close" msgid="9135220303720555525">"Loka mynd í mynd"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Allur skjárinn"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-it/strings.xml b/libs/WindowManager/Shell/res/values-it/strings.xml new file mode 100644 index 000000000000..8a2b9dbd9ba8 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-it/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Chiudi"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Espandi"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Impostazioni"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> è in Picture in picture"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Se non desideri che l\'app <xliff:g id="NAME">%s</xliff:g> utilizzi questa funzione, tocca per aprire le impostazioni e disattivarla."</string> + <string name="pip_play" msgid="3496151081459417097">"Riproduci"</string> + <string name="pip_pause" msgid="690688849510295232">"Metti in pausa"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Passa ai contenuti successivi"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Passa ai contenuti precedenti"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ridimensiona"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"L\'app potrebbe non funzionare con lo schermo diviso."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"L\'app non supporta la modalità Schermo diviso."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"L\'app potrebbe non funzionare su un display secondario."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"L\'app non supporta l\'avvio su display secondari."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Strumento per schermo diviso"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Schermata sinistra a schermo intero"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Schermata sinistra al 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Schermata sinistra al 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Schermata sinistra al 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Schermata destra a schermo intero"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Schermata superiore a schermo intero"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Schermata superiore al 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Schermata superiore al 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Schermata superiore al 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Schermata inferiore a schermo intero"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Usare la modalità one-hand"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Per uscire, scorri verso l\'alto dalla parte inferiore dello schermo oppure tocca un punto qualsiasi sopra l\'app"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Avvia la modalità one-hand"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Esci dalla modalità one-hand"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Impostazioni per bolle <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Altre"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Aggiungi di nuovo all\'elenco"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> da <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> da <xliff:g id="APP_NAME">%2$s</xliff:g> e altre <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Sposta in alto a sinistra"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Sposta in alto a destra"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Sposta in basso a sinistra"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Sposta in basso a destra"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Impostazioni <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ignora bolla"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Non mettere la conversazione nella bolla"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatta utilizzando le bolle"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Le nuove conversazioni vengono visualizzate come icone mobili o bolle. Tocca per aprire la bolla. Trascinala per spostarla."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Controlla le bolle quando vuoi"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Tocca Gestisci per disattivare le bolle dall\'app"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Nessuna bolla recente"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Le bolle recenti e ignorate appariranno qui"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Fumetto"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestisci"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Fumetto ignorato."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-it/strings_tv.xml b/libs/WindowManager/Shell/res/values-it/strings_tv.xml new file mode 100644 index 000000000000..127454cf28bf --- /dev/null +++ b/libs/WindowManager/Shell/res/values-it/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture in picture"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programma senza titolo)"</string> + <string name="pip_close" msgid="9135220303720555525">"Chiudi PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Schermo intero"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml new file mode 100644 index 000000000000..20114a7fa5f3 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-iw/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"סגירה"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"הרחב"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"הגדרות"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"תפריט"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> במצב תמונה בתוך תמונה"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"אם אינך רוצה שהתכונה הזו תשמש את <xliff:g id="NAME">%s</xliff:g>, יש להקיש כדי לפתוח את ההגדרות ולכבות את התכונה."</string> + <string name="pip_play" msgid="3496151081459417097">"הפעלה"</string> + <string name="pip_pause" msgid="690688849510295232">"השהה"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"אפשר לדלג אל הבא"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"אפשר לדלג אל הקודם"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"שינוי גודל"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"ייתכן שהאפליקציה לא תפעל במסך מפוצל."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"האפליקציה אינה תומכת במסך מפוצל."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ייתכן שהאפליקציה לא תפעל במסך משני."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"האפליקציה אינה תומכת בהפעלה במסכים משניים."</string> + <string name="accessibility_divider" msgid="703810061635792791">"מחלק מסך מפוצל"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"מסך שמאלי מלא"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"שמאלה 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"שמאלה 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"שמאלה 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"מסך ימני מלא"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"מסך עליון מלא"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"עליון 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"עליון 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"עליון 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"מסך תחתון מלא"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"איך להשתמש במצב שימוש ביד אחת"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"כדי לצאת, יש להחליק למעלה מתחתית המסך או להקיש במקום כלשהו במסך מעל האפליקציה"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"הפעלה של מצב שימוש ביד אחת"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"יציאה ממצב שימוש ביד אחת"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"הגדרות בשביל בועות של <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"גלישה"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"הוספה בחזרה לערימה"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> מהאפליקציה <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> מ-<xliff:g id="APP_NAME">%2$s</xliff:g> ועוד <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"העברה לפינה השמאלית העליונה"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"העברה לפינה הימנית העליונה"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"העברה לפינה השמאלית התחתונה"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"העברה לפינה הימנית התחתונה"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"הגדרות <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"סגירת בועה"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"אין להציג בועות לשיחה"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"לדבר בבועות"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"שיחות חדשות מופיעות כסמלים צפים, או בועות. יש להקיש כדי לפתוח בועה. יש לגרור כדי להזיז אותה."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"שליטה בבועות, בכל זמן"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"יש להקיש על \'ניהול\' כדי להשבית את הבועות מהאפליקציה הזו"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"הבנתי"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"אין בועות מהזמן האחרון"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"בועות אחרונות ובועות שנסגרו יופיעו כאן"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"בועה"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"ניהול"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"הבועה נסגרה."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-iw/strings_tv.xml b/libs/WindowManager/Shell/res/values-iw/strings_tv.xml new file mode 100644 index 000000000000..8ca54e0a5473 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-iw/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"תמונה בתוך תמונה"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(תוכנית ללא כותרת)"</string> + <string name="pip_close" msgid="9135220303720555525">"סגור PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"מסך מלא"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ja/strings.xml b/libs/WindowManager/Shell/res/values-ja/strings.xml new file mode 100644 index 000000000000..fbb2951a06e1 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ja/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"閉じる"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"展開"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"設定"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"メニュー"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>はピクチャー イン ピクチャーで表示中です"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g>でこの機能を使用しない場合は、タップして設定を開いて OFF にしてください。"</string> + <string name="pip_play" msgid="3496151081459417097">"再生"</string> + <string name="pip_pause" msgid="690688849510295232">"一時停止"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"次へスキップ"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"前へスキップ"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"サイズ変更"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"アプリは分割画面では動作しないことがあります。"</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"アプリで分割画面がサポートされていません。"</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"アプリはセカンダリ ディスプレイでは動作しないことがあります。"</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"アプリはセカンダリ ディスプレイでの起動に対応していません。"</string> + <string name="accessibility_divider" msgid="703810061635792791">"分割画面の分割線"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"左全画面"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"左 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"左 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"左 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"右全画面"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"上部全画面"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"上 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"上 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"上 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"下部全画面"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"片手モードの使用"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"終了するには、画面を下から上にスワイプするか、アプリの任意の場所をタップします"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"片手モードを開始します"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"片手モードを終了します"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> のバブルの設定"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"オーバーフロー"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"スタックに戻す"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>(<xliff:g id="APP_NAME">%2$s</xliff:g>)"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>(<xliff:g id="APP_NAME">%2$s</xliff:g>)、他 <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> 件"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"左上に移動"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"右上に移動"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"左下に移動"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"右下に移動"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> の設定"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"バブルを閉じる"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"会話をバブルで表示しない"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"チャットでバブルを使う"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"新しい会話はフローティング アイコン(バブル)として表示されます。タップするとバブルが開きます。ドラッグしてバブルを移動できます。"</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"いつでもバブルを管理"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"このアプリからのバブルを OFF にするには、[管理] をタップしてください"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"最近閉じたバブルはありません"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"最近表示されたバブルや閉じたバブルが、ここに表示されます"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"バブル"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ふきだしが非表示になっています。"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ja/strings_tv.xml b/libs/WindowManager/Shell/res/values-ja/strings_tv.xml new file mode 100644 index 000000000000..b7ab28c44fd2 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ja/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ピクチャー イン ピクチャー"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(無題の番組)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP を閉じる"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"全画面表示"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ka/strings.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml new file mode 100644 index 000000000000..f978481be23d --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ka/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"დახურვა"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"გაშლა"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"პარამეტრები"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"მენიუ"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> იყენებს რეჟიმს „ეკრანი ეკრანში“"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"თუ არ გსურთ, რომ <xliff:g id="NAME">%s</xliff:g> ამ ფუნქციას იყენებდეს, აქ შეხებით შეგიძლიათ გახსნათ პარამეტრები და გამორთოთ ის."</string> + <string name="pip_play" msgid="3496151081459417097">"დაკვრა"</string> + <string name="pip_pause" msgid="690688849510295232">"დაპაუზება"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"შემდეგზე გადასვლა"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"წინაზე გადასვლა"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ზომის შეცვლა"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"აპმა შეიძლება არ იმუშაოს გაყოფილი ეკრანის რეჟიმში."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ეკრანის გაყოფა არ არის მხარდაჭერილი აპის მიერ."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"აპმა შეიძლება არ იმუშაოს მეორეულ ეკრანზე."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"აპს არ გააჩნია მეორეული ეკრანის მხარდაჭერა."</string> + <string name="accessibility_divider" msgid="703810061635792791">"გაყოფილი ეკრანის რეჟიმის გამყოფი"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"მარცხენა ნაწილის სრულ ეკრანზე გაშლა"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"მარცხენა ეკრანი — 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"მარცხენა ეკრანი — 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"მარცხენა ეკრანი — 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"მარჯვენა ნაწილის სრულ ეკრანზე გაშლა"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"ზედა ნაწილის სრულ ეკრანზე გაშლა"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ზედა ეკრანი — 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ზედა ეკრანი — 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ზედა ეკრანი — 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"ქვედა ნაწილის სრულ ეკრანზე გაშლა"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"ცალი ხელის რეჟიმის გამოყენება"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"გასასვლელად გადაფურცლეთ ეკრანის ქვედა კიდიდან ზემოთ ან შეეხეთ ნებისმიერ ადგილას აპის ზემოთ"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"ცალი ხელის რეჟიმის დაწყება"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"ცალი ხელის რეჟიმიდან გამოსვლა"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"პარამეტრები <xliff:g id="APP_NAME">%1$s</xliff:g> ბუშტებისთვის"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"გადავსება"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"ისევ დამატება დასტაზე"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> <xliff:g id="APP_NAME">%2$s</xliff:g>-ისგან"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> <xliff:g id="APP_NAME">%2$s</xliff:g>-დან და კიდევ <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"ზევით და მარცხნივ გადატანა"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"გადაანაცვლეთ ზევით და მარჯვნივ"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"ქვევით და მარცხნივ გადატანა"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"გადაანაცვ. ქვემოთ და მარჯვნივ"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>-ის პარამეტრები"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"ბუშტის დახურვა"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"აიკრძალოს საუბრის ბუშტები"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"ჩეთი ბუშტების გამოყენებით"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"ახალი საუბრები გამოჩნდება როგორც მოტივტივე ხატულები ან ბუშტები. შეეხეთ ბუშტის გასახსნელად. გადაიტანეთ ჩავლებით."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"ბუშტების ნებისმიერ დროს გაკონტროლება"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"ამ აპის ბუშტების გამოსართავად შეეხეთ „მართვას“"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"გასაგებია"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"ბოლო დროს გამოყენებული ბუშტები არ არის"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"აქ გამოჩნდება ბოლოდროინდელი ბუშტები და უარყოფილი ბუშტები"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"ბუშტი"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"მართვა"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ბუშტი დაიხურა."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ka/strings_tv.xml b/libs/WindowManager/Shell/res/values-ka/strings_tv.xml new file mode 100644 index 000000000000..1bf4b8ebdcda --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ka/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ეკრანი ეკრანში"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(პროგრამის სათაურის გარეშე)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP-ის დახურვა"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"სრულ ეკრანზე"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-kk/strings.xml b/libs/WindowManager/Shell/res/values-kk/strings.xml new file mode 100644 index 000000000000..2d27fafcc98b --- /dev/null +++ b/libs/WindowManager/Shell/res/values-kk/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Жабу"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Жаю"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Параметрлер"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Mәзір"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> \"суреттегі сурет\" режимінде"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> деген пайдаланушының бұл мүмкіндікті пайдалануын қаламасаңыз, параметрлерді түртіп ашыңыз да, оларды өшіріңіз."</string> + <string name="pip_play" msgid="3496151081459417097">"Ойнату"</string> + <string name="pip_pause" msgid="690688849510295232">"Кідірту"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Келесіге өту"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Алдыңғысына оралу"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Өлшемін өзгерту"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Қолданба экранды бөлу режимінде жұмыс істемеуі мүмкін."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Қодланба бөлінген экранды қолдамайды."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Қолданба қосымша дисплейде жұмыс істемеуі мүмкін."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Қолданба қосымша дисплейлерде іске қосуды қолдамайды."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Бөлінген экран бөлгіші"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Сол жағын толық экранға шығару"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70% сол жақта"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50% сол жақта"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"30% сол жақта"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Оң жағын толық экранға шығару"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Жоғарғы жағын толық экранға шығару"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"70% жоғарғы жақта"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50% жоғарғы жақта"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"30% жоғарғы жақта"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Төменгісін толық экранға шығару"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Бір қолмен енгізу режимін пайдалану"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Шығу үшін экранның төменгі жағынан жоғары қарай сырғытыңыз немесе қолданбаның үстінен кез келген жерден түртіңіз."</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Бір қолмен енгізу режимін іске қосу"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Бір қолмен енгізу режимінен шығу"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> қалқыма хабарларының параметрлері"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Қосымша мәзір"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Стекке қайта енгізу"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g> жіберген хабарландыру: <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> қолданбасы жіберген <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> және тағы <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Жоғарғы сол жаққа жылжыту"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Жоғары оң жаққа жылжыту"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Төменгі сол жаққа жылжыту"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Төменгі оң жаққа жылжыту"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> параметрлері"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Қалқымалы хабарды жабу"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Әңгіменің қалқыма хабары көрсетілмесін"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Қалқыма хабарлар арқылы сөйлесу"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Жаңа әңгімелер қалқыма белгішелер немесе хабарлар түрінде көрсетіледі. Қалқыма хабарды ашу үшін түртіңіз. Жылжыту үшін сүйреңіз."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Қалқыма хабарларды реттеу"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Бұл қолданбадан қалқыма хабарларды өшіру үшін \"Басқару\" түймесін түртіңіз."</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Түсінікті"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Жақындағы қалқыма хабарлар жоқ"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Соңғы және жабылған қалқыма хабарлар осы жерде көрсетіледі."</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Көпіршік"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Басқару"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Қалқымалы анықтама өшірілді."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-kk/strings_tv.xml b/libs/WindowManager/Shell/res/values-kk/strings_tv.xml new file mode 100644 index 000000000000..8f1e725e79e2 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-kk/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Суреттегі сурет"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Атаусыз бағдарлама)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP жабу"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Толық экран"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-km/strings.xml b/libs/WindowManager/Shell/res/values-km/strings.xml new file mode 100644 index 000000000000..d503b7a5edca --- /dev/null +++ b/libs/WindowManager/Shell/res/values-km/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"បិទ"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"ពង្រីក"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"ការកំណត់"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"ម៉ឺនុយ"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ស្ថិតក្នុងមុខងាររូបក្នុងរូប"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"ប្រសិនបើអ្នកមិនចង់ឲ្យ <xliff:g id="NAME">%s</xliff:g> ប្រើមុខងារនេះ សូមចុចបើកការកំណត់ រួចបិទវា។"</string> + <string name="pip_play" msgid="3496151081459417097">"លេង"</string> + <string name="pip_pause" msgid="690688849510295232">"ផ្អាក"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"រំលងទៅបន្ទាប់"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"រំលងទៅក្រោយ"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ប្ដូរទំហំ"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"កម្មវិធីអាចនឹងមិនដំណើរការជាមួយមុខងារបំបែកអេក្រង់ទេ។"</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"កម្មវិធីមិនគាំទ្រអេក្រង់បំបែកជាពីរទេ"</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"កម្មវិធីនេះប្រហែលជាមិនដំណើរការនៅលើអេក្រង់បន្ទាប់បន្សំទេ។"</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"កម្មវិធីនេះមិនអាចចាប់ផ្តើមនៅលើអេក្រង់បន្ទាប់បន្សំបានទេ។"</string> + <string name="accessibility_divider" msgid="703810061635792791">"កម្មវិធីចែកអេក្រង់បំបែក"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"អេក្រង់ពេញខាងឆ្វេង"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ឆ្វេង 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ឆ្វេង 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ឆ្វេង 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"អេក្រង់ពេញខាងស្តាំ"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"អេក្រង់ពេញខាងលើ"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ខាងលើ 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ខាងលើ 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ខាងលើ 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"អេក្រង់ពេញខាងក្រោម"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"កំពុងប្រើមុខងារប្រើដៃម្ខាង"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"ដើម្បីចាកចេញ សូមអូសឡើងលើពីផ្នែកខាងក្រោមអេក្រង់ ឬចុចផ្នែកណាមួយនៅខាងលើកម្មវិធី"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"ចាប់ផ្ដើមមុខងារប្រើដៃម្ខាង"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"ចាកចេញពីមុខងារប្រើដៃម្ខាង"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"ការកំណត់សម្រាប់ពពុះ <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"ម៉ឺនុយបន្ថែម"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"បញ្ចូលទៅក្នុងគំនរវិញ"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ពី <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ពី <xliff:g id="APP_NAME">%2$s</xliff:g> និង <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> ទៀត"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"ផ្លាស់ទីទៅផ្នែកខាងលើខាងឆ្វេង"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ផ្លាស់ទីទៅផ្នែកខាងលើខាងស្ដាំ"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"ផ្លាស់ទីទៅផ្នែកខាងក្រោមខាងឆ្វេង"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ផ្លាស់ទីទៅផ្នែកខាងក្រោមខាងស្ដាំ"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"ការកំណត់ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"ច្រានចោលពពុះ"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"កុំបង្ហាញការសន្ទនាជាពពុះ"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"ជជែកដោយប្រើពពុះ"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"ការសន្ទនាថ្មីៗបង្ហាញជាពពុះ ឬរូបអណ្ដែត។ ចុច ដើម្បីបើកពពុះ។ អូស ដើម្បីផ្លាស់ទីពពុះនេះ។"</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"គ្រប់គ្រងពពុះបានគ្រប់ពេល"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"ចុច \"គ្រប់គ្រង\" ដើម្បីបិទពពុះពីកម្មវិធីនេះ"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"យល់ហើយ"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"មិនមានពពុះថ្មីៗទេ"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"ពពុះថ្មីៗ និងពពុះដែលបានបិទនឹងបង្ហាញនៅទីនេះ"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"ពពុះ"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"គ្រប់គ្រង"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"បានច្រានចោលសារលេចឡើង។"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-km/strings_tv.xml b/libs/WindowManager/Shell/res/values-km/strings_tv.xml new file mode 100644 index 000000000000..b55997056e66 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-km/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"រូបក្នុងរូប"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(កម្មវិធីគ្មានចំណងជើង)"</string> + <string name="pip_close" msgid="9135220303720555525">"បិទ PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"ពេញអេក្រង់"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-kn/strings.xml b/libs/WindowManager/Shell/res/values-kn/strings.xml new file mode 100644 index 000000000000..3d61d84f4810 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-kn/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"ಮುಚ್ಚಿ"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"ವಿಸ್ತೃತಗೊಳಿಸು"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"ಸೆಟ್ಟಿಂಗ್ಗಳು"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"ಮೆನು"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರವಾಗಿದೆ"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ಈ ವೈಶಿಷ್ಟ್ಯ ಬಳಸುವುದನ್ನು ನೀವು ಬಯಸದಿದ್ದರೆ, ಸೆಟ್ಟಿಂಗ್ಗಳನ್ನು ತೆರೆಯಲು ಮತ್ತು ಅದನ್ನು ಆಫ್ ಮಾಡಲು ಟ್ಯಾಪ್ ಮಾಡಿ."</string> + <string name="pip_play" msgid="3496151081459417097">"ಪ್ಲೇ"</string> + <string name="pip_pause" msgid="690688849510295232">"ವಿರಾಮಗೊಳಿಸಿ"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"ಮುಂದಕ್ಕೆ ಸ್ಕಿಪ್ ಮಾಡಿ"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"ಹಿಂದಕ್ಕೆ ಸ್ಕಿಪ್ ಮಾಡಿ"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ಮರುಗಾತ್ರಗೊಳಿಸಿ"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"ವಿಭಜಿಸಿದ ಸ್ಕ್ರೀನ್ನಲ್ಲಿ ಆ್ಯಪ್ ಕೆಲಸ ಮಾಡದೇ ಇರಬಹುದು."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ಅಪ್ಲಿಕೇಶನ್ ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್ ಅನ್ನು ಬೆಂಬಲಿಸುವುದಿಲ್ಲ."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ಸೆಕೆಂಡರಿ ಡಿಸ್ಪ್ಲೇಗಳಲ್ಲಿ ಅಪ್ಲಿಕೇಶನ್ ಕಾರ್ಯ ನಿರ್ವಹಿಸದೇ ಇರಬಹುದು."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ಸೆಕೆಂಡರಿ ಡಿಸ್ಪ್ಲೇಗಳಲ್ಲಿ ಪ್ರಾರಂಭಿಸುವಿಕೆಯನ್ನು ಅಪ್ಲಿಕೇಶನ್ ಬೆಂಬಲಿಸುವುದಿಲ್ಲ."</string> + <string name="accessibility_divider" msgid="703810061635792791">"ಸ್ಪ್ಲಿಟ್-ಪರದೆ ಡಿವೈಡರ್"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ಎಡ ಪೂರ್ಣ ಪರದೆ"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70% ಎಡಕ್ಕೆ"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50% ಎಡಕ್ಕೆ"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"30% ಎಡಕ್ಕೆ"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"ಬಲ ಪೂರ್ಣ ಪರದೆ"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"ಮೇಲಿನ ಪೂರ್ಣ ಪರದೆ"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"70% ಮೇಲಕ್ಕೆ"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50% ಮೇಲಕ್ಕೆ"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"30% ಮೇಲಕ್ಕೆ"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"ಕೆಳಗಿನ ಪೂರ್ಣ ಪರದೆ"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"ಒಂದು ಕೈ ಮೋಡ್ ಬಳಸುವುದರ ಬಗ್ಗೆ"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"ನಿರ್ಗಮಿಸಲು, ಸ್ಕ್ರೀನ್ನ ಕೆಳಗಿನಿಂದ ಮೇಲಕ್ಕೆ ಸ್ವೈಪ್ ಮಾಡಿ ಅಥವಾ ಆ್ಯಪ್ನ ಮೇಲೆ ಎಲ್ಲಿಯಾದರೂ ಟ್ಯಾಪ್ ಮಾಡಿ"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"ಒಂದು ಕೈ ಮೋಡ್ ಅನ್ನು ಪ್ರಾರಂಭಿಸಿ"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"ಒಂದು ಕೈ ಮೋಡ್ನಿಂದ ನಿರ್ಗಮಿಸಿ"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> ಬಬಲ್ಸ್ಗಾಗಿ ಸೆಟ್ಟಿಂಗ್ಗಳು"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"ಓವರ್ಫ್ಲೋ"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"ಸ್ಟ್ಯಾಕ್ಗೆ ಪುನಃ ಸೇರಿಸಿ"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g> ಆ್ಯಪ್ನ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> ಮತ್ತು <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> ಹೆಚ್ಚಿನವುಗಳ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"ಎಡ ಮೇಲ್ಭಾಗಕ್ಕೆ ಸರಿಸಿ"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ಬಲ ಮೇಲ್ಭಾಗಕ್ಕೆ ಸರಿಸಿ"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"ಸ್ಕ್ರೀನ್ನ ಎಡ ಕೆಳಭಾಗಕ್ಕೆ ಸರಿಸಿ"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ಕೆಳಗಿನ ಬಲಭಾಗಕ್ಕೆ ಸರಿಸಿ"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ಸೆಟ್ಟಿಂಗ್ಗಳು"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"ಬಬಲ್ ವಜಾಗೊಳಿಸಿ"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"ಸಂಭಾಷಣೆಯನ್ನು ಬಬಲ್ ಮಾಡಬೇಡಿ"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"ಬಬಲ್ಸ್ ಬಳಸಿ ಚಾಟ್ ಮಾಡಿ"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"ಹೊಸ ಸಂಭಾಷಣೆಗಳು ತೇಲುವ ಐಕಾನ್ಗಳು ಅಥವಾ ಬಬಲ್ಸ್ ಆಗಿ ಗೋಚರಿಸುತ್ತವೆ. ಬಬಲ್ ತೆರೆಯಲು ಟ್ಯಾಪ್ ಮಾಡಿ. ಅದನ್ನು ಡ್ರ್ಯಾಗ್ ಮಾಡಲು ಎಳೆಯಿರಿ."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"ಯಾವುದೇ ಸಮಯದಲ್ಲಿ ಬಬಲ್ಸ್ ಅನ್ನು ನಿಯಂತ್ರಿಸಿ"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"ಈ ಆ್ಯಪ್ನಿಂದ ಬಬಲ್ಸ್ ಅನ್ನು ಆಫ್ ಮಾಡಲು ನಿರ್ವಹಿಸಿ ಟ್ಯಾಪ್ ಮಾಡಿ"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"ಅರ್ಥವಾಯಿತು"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"ಯಾವುದೇ ಇತ್ತೀಚಿನ ಬಬಲ್ಸ್ ಇಲ್ಲ"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"ಇತ್ತೀಚಿನ ಬಬಲ್ಸ್ ಮತ್ತು ವಜಾಗೊಳಿಸಿದ ಬಬಲ್ಸ್ ಇಲ್ಲಿ ಗೋಚರಿಸುತ್ತವೆ"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"ಬಬಲ್"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"ನಿರ್ವಹಿಸಿ"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ಬಬಲ್ ವಜಾಗೊಳಿಸಲಾಗಿದೆ."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-kn/strings_tv.xml b/libs/WindowManager/Shell/res/values-kn/strings_tv.xml new file mode 100644 index 000000000000..9d3942fa4dd3 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-kn/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರ"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ಶೀರ್ಷಿಕೆ ರಹಿತ ಕಾರ್ಯಕ್ರಮ)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP ಮುಚ್ಚಿ"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"ಪೂರ್ಣ ಪರದೆ"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ko/strings.xml b/libs/WindowManager/Shell/res/values-ko/strings.xml new file mode 100644 index 000000000000..ea7ad56bf9d2 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ko/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"닫기"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"펼치기"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"설정"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"메뉴"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>에서 PIP 사용 중"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g>에서 이 기능이 사용되는 것을 원하지 않는 경우 탭하여 설정을 열고 기능을 사용 중지하세요."</string> + <string name="pip_play" msgid="3496151081459417097">"재생"</string> + <string name="pip_pause" msgid="690688849510295232">"일시중지"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"다음으로 건너뛰기"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"이전으로 건너뛰기"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"크기 조절"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"앱이 분할 화면에서 작동하지 않을 수 있습니다."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"앱이 화면 분할을 지원하지 않습니다."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"앱이 보조 디스플레이에서 작동하지 않을 수도 있습니다."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"앱이 보조 디스플레이에서의 실행을 지원하지 않습니다."</string> + <string name="accessibility_divider" msgid="703810061635792791">"화면 분할기"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"왼쪽 화면 전체화면"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"왼쪽 화면 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"왼쪽 화면 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"왼쪽 화면 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"오른쪽 화면 전체화면"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"위쪽 화면 전체화면"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"위쪽 화면 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"위쪽 화면 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"위쪽 화면 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"아래쪽 화면 전체화면"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"한 손 사용 모드 사용하기"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"화면 하단에서 위로 스와이프하거나 앱 상단을 탭하여 종료합니다."</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"한 손 사용 모드 시작"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"한 손 사용 모드 종료"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> 대화창 설정"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"더보기"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"스택에 다시 추가"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g>의 <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> 외 <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>개의 <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"왼쪽 상단으로 이동"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"오른쪽 상단으로 이동"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"왼쪽 하단으로 이동"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"오른쪽 하단으로 이동"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> 설정"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"대화창 닫기"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"대화를 대화창으로 표시하지 않기"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"대화창으로 채팅하기"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"새로운 대화가 플로팅 아이콘인 대화창으로 표시됩니다. 대화창을 열려면 탭하세요. 드래그하여 이동할 수 있습니다."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"언제든지 대화창을 제어하세요"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"이 앱에서 대화창을 사용 중지하려면 관리를 탭하세요."</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"확인"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"최근 대화창 없음"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"최근 대화창과 내가 닫은 대화창이 여기에 표시됩니다."</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"버블"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"관리"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"대화창을 닫았습니다."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ko/strings_tv.xml b/libs/WindowManager/Shell/res/values-ko/strings_tv.xml new file mode 100644 index 000000000000..46d6ad4e0b0f --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ko/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"PIP 모드"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(제목 없는 프로그램)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP 닫기"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"전체화면"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ky/strings.xml b/libs/WindowManager/Shell/res/values-ky/strings.xml new file mode 100644 index 000000000000..611b2d60a8c1 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ky/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Жабуу"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Жайып көрсөтүү"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Жөндөөлөр"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Меню"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> – сүрөт ичиндеги сүрөт"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Эгер <xliff:g id="NAME">%s</xliff:g> колдонмосу бул функцияны пайдаланбасын десеңиз, жөндөөлөрдү ачып туруп, аны өчүрүп коюңуз."</string> + <string name="pip_play" msgid="3496151081459417097">"Ойнотуу"</string> + <string name="pip_pause" msgid="690688849510295232">"Тындыруу"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Кийинкисине өткөрүп жиберүү"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Мурункусуна өткөрүп жиберүү"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Өлчөмүн өзгөртүү"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Колдонмодо экран бөлүнбөшү мүмкүн."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Колдонмодо экран бөлүнбөйт."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Колдонмо кошумча экранда иштебей коюшу мүмкүн."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Колдонмону кошумча экрандарда иштетүүгө болбойт."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Экранды бөлгүч"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Сол жактагы экранды толук экран режимине өткөрүү"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Сол жактагы экранды 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Сол жактагы экранды 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Сол жактагы экранды 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Оң жактагы экранды толук экран режимине өткөрүү"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Үстүнкү экранды толук экран режимине өткөрүү"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Үстүнкү экранды 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Үстүнкү экранды 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Үстүнкү экранды 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Ылдыйкы экранды толук экран режимине өткөрүү"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Бир кол режимин колдонуу"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Чыгуу үчүн экранды ылдый жагынан өйдө көздөй сүрүңүз же колдонмонун өйдө жагын басыңыз"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Бир кол режимин баштоо"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Бир кол режиминен чыгуу"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> калкып чыкма билдирмелер жөндөөлөрү"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Кошумча меню"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Кайра топтомго кошуу"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g> колдонмосунан <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> жана дагы <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> колдонмодон <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Жогорку сол жакка жылдыруу"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Жогорку оң жакка жылдырыңыз"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Төмөнкү сол жакка жылдыруу"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Төмөнкү оң жакка жылдырыңыз"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> жөндөөлөрү"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Калкып чыкма билдирмени жабуу"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Жазышууда калкып чыкма билдирмелер көрүнбөсүн"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Калкып чыкма билдирмелер аркылуу маектешүү"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Жаңы жазышуулар калкыма сүрөтчөлөр же калкып чыкма билдирмелер түрүндө көрүнөт. Калкып чыкма билдирмелерди ачуу үчүн таптап коюңуз. Жылдыруу үчүн сүйрөңүз."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Калкып чыкма билдирмелерди каалаган убакта көзөмөлдөңүз"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Бул колдонмодогу калкып чыкма билдирмелерди өчүрүү үчүн, \"Башкарууну\" басыңыз"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Түшүндүм"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Азырынча эч нерсе жок"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Акыркы жана жабылган калкып чыкма билдирмелер ушул жерде көрүнөт"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Көбүк"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Башкаруу"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Калкып чыкма билдирме жабылды."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ky/strings_tv.xml b/libs/WindowManager/Shell/res/values-ky/strings_tv.xml new file mode 100644 index 000000000000..d5d1d7ef914e --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ky/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Сүрөттөгү сүрөт"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Аталышы жок программа)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP\'ти жабуу"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Толук экран"</string> +</resources> 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..aafba58cef59 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-land/dimens.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. +*/ +--> +<resources> + <dimen name="docked_divider_handle_width">2dp</dimen> + <dimen name="docked_divider_handle_height">16dp</dimen> + + <!-- Padding between status bar and bubbles when displayed in expanded state, smaller + value in landscape since we have limited vertical space--> + <dimen name="bubble_padding_top">4dp</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-lo/strings.xml b/libs/WindowManager/Shell/res/values-lo/strings.xml new file mode 100644 index 000000000000..a1c998c078de --- /dev/null +++ b/libs/WindowManager/Shell/res/values-lo/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"ປິດ"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"ຂະຫຍາຍ"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"ການຕັ້ງຄ່າ"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"ເມນູ"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ແມ່ນເປັນການສະແດງຜົນຫຼາຍຢ່າງພ້ອມກັນ"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"ຫາກທ່ານບໍ່ຕ້ອງການ <xliff:g id="NAME">%s</xliff:g> ໃຫ້ໃຊ້ຄຸນສົມບັດນີ້, ໃຫ້ແຕະເພື່ອເປີດການຕັ້ງຄ່າ ແລ້ວປິດມັນໄວ້."</string> + <string name="pip_play" msgid="3496151081459417097">"ຫຼິ້ນ"</string> + <string name="pip_pause" msgid="690688849510295232">"ຢຸດຊົ່ວຄາວ"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"ຂ້າມໄປລາຍການໜ້າ"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"ຂ້າມໄປລາຍການກ່ອນນີ້"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ປ່ຽນຂະໜາດ"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"ແອັບອາດໃຊ້ບໍ່ໄດ້ກັບການແບ່ງໜ້າຈໍ."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ແອັບບໍ່ຮອງຮັບໜ້າຈໍແບບແຍກກັນ."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ແອັບອາດບໍ່ສາມາດໃຊ້ໄດ້ໃນໜ້າຈໍທີສອງ."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ແອັບບໍ່ຮອງຮັບການເປີດໃນໜ້າຈໍທີສອງ."</string> + <string name="accessibility_divider" msgid="703810061635792791">"ຕົວຂັ້ນການແບ່ງໜ້າຈໍ"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ເຕັມໜ້າຈໍຊ້າຍ"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ຊ້າຍ 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ຊ້າຍ 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ຊ້າຍ 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"ເຕັມໜ້າຈໍຂວາ"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"ເຕັມໜ້າຈໍເທິງສຸດ"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ເທິງສຸດ 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ເທິງສຸດ 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ເທິງສຸດ 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"ເຕັມໜ້າຈໍລຸ່ມສຸດ"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"ກຳລັງໃຊ້ໂໝດມືດຽວ"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"ເພື່ອອອກ, ໃຫ້ປັດຂຶ້ນຈາກລຸ່ມສຸດຂອງໜ້າຈໍ ຫຼື ແຕະບ່ອນໃດກໍໄດ້ຢູ່ເໜືອແອັບ"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"ເລີ່ມໂໝດມືດຽວ"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"ອອກຈາກໂໝດມືດຽວ"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"ການຕັ້ງຄ່າສຳລັບຟອງ <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"ລົ້ນ"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"ເພີ່ມກັບໄປຫາການວາງຊ້ອນກັນ"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ຈາກ <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ຈາກ <xliff:g id="APP_NAME">%2$s</xliff:g> ແລະ ອີກ <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"ຍ້າຍຊ້າຍເທິງ"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ຍ້າຍຂວາເທິງ"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"ຍ້າຍຊ້າຍລຸ່ມ"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ຍ້າຍຂວາລຸ່ມ"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"ການຕັ້ງຄ່າ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"ປິດຟອງໄວ້"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"ຢ່າໃຊ້ຟອງໃນການສົນທະນາ"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"ສົນທະນາໂດຍໃຊ້ຟອງ"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"ການສົນທະນາໃໝ່ຈະປາກົດເປັນໄອຄອນ ຫຼື ຟອງແບບລອຍ. ແຕະເພື່ອເປີດຟອງ. ລາກເພື່ອຍ້າຍມັນ."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"ຄວບຄຸມຟອງຕອນໃດກໍໄດ້"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"ແຕະຈັດການ ເພື່ອປິດຟອງຈາກແອັບນີ້"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"ເຂົ້າໃຈແລ້ວ"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"ບໍ່ມີຟອງຫຼ້າສຸດ"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"ຟອງຫຼ້າສຸດ ແລະ ຟອງທີ່ປິດໄປຈະປາກົດຢູ່ບ່ອນນີ້"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"ຟອງ"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"ຈັດການ"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ປິດ Bubble ໄສ້ແລ້ວ."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-lo/strings_tv.xml b/libs/WindowManager/Shell/res/values-lo/strings_tv.xml new file mode 100644 index 000000000000..f6362c120b9f --- /dev/null +++ b/libs/WindowManager/Shell/res/values-lo/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ການສະແດງຜົນຊ້ອນກັນ"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ໂປຣແກຣມບໍ່ມີຊື່)"</string> + <string name="pip_close" msgid="9135220303720555525">"ປິດ PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"ເຕັມໜ້າຈໍ"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml new file mode 100644 index 000000000000..b2ccd5709e21 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-lt/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Uždaryti"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Išskleisti"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Nustatymai"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Meniu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> rodom. vaizdo vaizde"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Jei nenorite, kad „<xliff:g id="NAME">%s</xliff:g>“ naudotų šią funkciją, palietę atidarykite nustatymus ir išjunkite ją."</string> + <string name="pip_play" msgid="3496151081459417097">"Leisti"</string> + <string name="pip_pause" msgid="690688849510295232">"Pristabdyti"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Praleisti ir eiti į kitą"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Praleisti ir eiti į ankstesnį"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Pakeisti dydį"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Programa gali neveikti naudojant išskaidyto ekrano režimą."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Programoje nepalaikomas skaidytas ekranas."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Programa gali neveikti antriniame ekrane."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Programa nepalaiko paleisties antriniuose ekranuose."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Skaidyto ekrano daliklis"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Kairysis ekranas viso ekrano režimu"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Kairysis ekranas 70 %"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Kairysis ekranas 50 %"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Kairysis ekranas 30 %"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Dešinysis ekranas viso ekrano režimu"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Viršutinis ekranas viso ekrano režimu"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Viršutinis ekranas 70 %"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Viršutinis ekranas 50 %"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Viršutinis ekranas 30 %"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Apatinis ekranas viso ekrano režimu"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Vienos rankos režimo naudojimas"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Jei norite išeiti, perbraukite aukštyn nuo ekrano apačios arba palieskite bet kur virš programos"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Pradėti vienos rankos režimą"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Išeiti iš vienos rankos režimo"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"„<xliff:g id="APP_NAME">%1$s</xliff:g>“ burbulų nustatymai"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Perpildymas"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Pridėti atgal į krūvą"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"„<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>“ iš „<xliff:g id="APP_NAME">%2$s</xliff:g>“"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"„<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>“ iš „<xliff:g id="APP_NAME">%2$s</xliff:g>“ ir dar <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Perkelti į viršų kairėje"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Perkelti į viršų dešinėje"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Perkelti į apačią kairėje"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Perkelti į apačią dešinėje"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"„<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>“ nustatymai"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Atsisakyti burbulo"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nerodyti pokalbio burbule"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Pokalbis naudojant burbulus"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nauji pokalbiai rodomi kaip slankiosios piktogramos arba burbulai. Palieskite, kad atidarytumėte burbulą. Vilkite, kad perkeltumėte."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Bet kada valdyti burbulus"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Palieskite „Tvarkyti“, kad išjungtumėte burbulus šioje programoje"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Supratau"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Nėra naujausių burbulų"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Naujausi ir atsisakyti burbulai bus rodomi čia"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Debesėlis"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Tvarkyti"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Debesėlio atsisakyta."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-lt/strings_tv.xml b/libs/WindowManager/Shell/res/values-lt/strings_tv.xml new file mode 100644 index 000000000000..e4695a05f038 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-lt/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Vaizdas vaizde"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programa be pavadinimo)"</string> + <string name="pip_close" msgid="9135220303720555525">"Uždaryti PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Visas ekranas"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-lv/strings.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml new file mode 100644 index 000000000000..e6d0c7725bbf --- /dev/null +++ b/libs/WindowManager/Shell/res/values-lv/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Aizvērt"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Izvērst"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Iestatījumi"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Izvēlne"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ir attēlā attēlā"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Ja nevēlaties lietotnē <xliff:g id="NAME">%s</xliff:g> izmantot šo funkciju, pieskarieties, lai atvērtu iestatījumus un izslēgtu funkciju."</string> + <string name="pip_play" msgid="3496151081459417097">"Atskaņot"</string> + <string name="pip_pause" msgid="690688849510295232">"Apturēt"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Pāriet uz nākamo"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Pāriet uz iepriekšējo"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Mainīt lielumu"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Iespējams, lietotne nedarbosies ekrāna sadalīšanas režīmā."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Lietotnē netiek atbalstīta ekrāna sadalīšana."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Lietotne, iespējams, nedarbosies sekundārajā displejā."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Lietotnē netiek atbalstīta palaišana sekundārajos displejos."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Ekrāna sadalītājs"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Kreisā daļa pa visu ekrānu"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Pa kreisi 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Pa kreisi 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Pa kreisi 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Labā daļa pa visu ekrānu"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Augšdaļa pa visu ekrānu"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Augšdaļa 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Augšdaļa 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Augšdaļa 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Apakšdaļu pa visu ekrānu"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Vienas rokas režīma izmantošana"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Lai izietu, velciet augšup no ekrāna apakšdaļas vai pieskarieties jebkurā vietā virs lietotnes"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Pāriet vienas rokas režīmā"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Iziet no vienas rokas režīma"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Lietotnes <xliff:g id="APP_NAME">%1$s</xliff:g> burbuļu iestatījumi"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Pārpilde"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Pievienot atpakaļ kopai"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> no: <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> no lietotnes “<xliff:g id="APP_NAME">%2$s</xliff:g>” un vēl <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Pārvietot augšpusē pa kreisi"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Pārvietot augšpusē pa labi"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Pārvietot apakšpusē pa kreisi"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Pārvietot apakšpusē pa labi"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Lietotnes <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> iestatījumi"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Nerādīt burbuli"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nerādīt sarunu burbuļos"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Tērzēšana, izmantojot burbuļus"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Jaunas sarunas tiek rādītas kā peldošas ikonas vai burbuļi. Pieskarieties, lai atvērtu burbuli. Velciet, lai to pārvietotu."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Allaž pārvaldīt burbuļus"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Pieskarieties pogai “Pārvaldīt”, lai izslēgtu burbuļus no šīs lietotnes."</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Labi"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Nav nesen aizvērtu burbuļu"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Šeit būs redzami nesen rādītie burbuļi un aizvērtie burbuļi"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Burbulis"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Pārvaldīt"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Burbulis ir noraidīts."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-lv/strings_tv.xml b/libs/WindowManager/Shell/res/values-lv/strings_tv.xml new file mode 100644 index 000000000000..f2b037fbeeee --- /dev/null +++ b/libs/WindowManager/Shell/res/values-lv/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Attēls attēlā"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programma bez nosaukuma)"</string> + <string name="pip_close" msgid="9135220303720555525">"Aizvērt PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Pilnekrāna režīms"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-mk/strings.xml b/libs/WindowManager/Shell/res/values-mk/strings.xml new file mode 100644 index 000000000000..43f2881fd553 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-mk/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Затвори"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Проширете"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Поставки"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Мени"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> е во слика во слика"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Ако не сакате <xliff:g id="NAME">%s</xliff:g> да ја користи функцијава, допрете за да ги отворите поставките и да ја исклучите."</string> + <string name="pip_play" msgid="3496151081459417097">"Пушти"</string> + <string name="pip_pause" msgid="690688849510295232">"Паузирај"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Прескокни до следната"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Прескокни до претходната"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Промени големина"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Апликацијата може да не работи со поделен екран."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Апликацијата не поддржува поделен екран."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Апликацијата може да не функционира на друг екран."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Апликацијата не поддржува стартување на други екрани."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Разделник на поделен екран"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Левиот на цел екран"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Левиот 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Левиот 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Левиот 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Десниот на цел екран"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Горниот на цел екран"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Горниот 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Горниот 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Горниот 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Долниот на цел екран"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Користење на режимот со една рака"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"За да излезете, повлечете нагоре од дното на екранот или допрете каде било над апликацијата"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Започни го режимот со една рака"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Излези од режимот со една рака"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Поставки за балончињата за <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Прелевање"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Додајте назад во stack"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> од <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> од <xliff:g id="APP_NAME">%2$s</xliff:g> и уште <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Премести горе лево"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Премести горе десно"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Премести долу лево"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Премести долу десно"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Поставки за <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Отфрли балонче"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Не прикажувај го разговорот во балончиња"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Разговор во балончиња"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Новите разговори ќе се појавуваат како лебдечки икони или балончиња. Допрете за отворање на балончето. Повлечете за да го преместите."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Контролирајте ги балончињата во секое време"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Допрете „Управувајте“ за да ги исклучите балончињата од апликацијава"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Сфатив"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Нема неодамнешни балончиња"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Неодамнешните и отфрлените балончиња ќе се појавуваат тука"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Балонче"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Управувајте"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Балончето е отфрлено."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-mk/strings_tv.xml b/libs/WindowManager/Shell/res/values-mk/strings_tv.xml new file mode 100644 index 000000000000..25dc764f4d5e --- /dev/null +++ b/libs/WindowManager/Shell/res/values-mk/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Слика во слика"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Програма без наслов)"</string> + <string name="pip_close" msgid="9135220303720555525">"Затвори PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Цел екран"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ml/strings.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml new file mode 100644 index 000000000000..e675861166a3 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ml/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"അവസാനിപ്പിക്കുക"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"വികസിപ്പിക്കുക"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"ക്രമീകരണം"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"മെനു"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ചിത്രത്തിനുള്ളിൽ ചിത്രം രീതിയിലാണ്"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ഈ ഫീച്ചർ ഉപയോഗിക്കേണ്ടെങ്കിൽ, ടാപ്പ് ചെയ്ത് ക്രമീകരണം തുറന്ന് അത് ഓഫാക്കുക."</string> + <string name="pip_play" msgid="3496151081459417097">"പ്ലേ ചെയ്യുക"</string> + <string name="pip_pause" msgid="690688849510295232">"താൽക്കാലികമായി നിർത്തുക"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"അടുത്തതിലേക്ക് പോകുക"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"മുമ്പത്തേതിലേക്ക് പോകുക"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"വലുപ്പം മാറ്റുക"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"സ്ക്രീൻ വിഭജന മോഡിൽ ആപ്പ് പ്രവർത്തിച്ചേക്കില്ല."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"സ്പ്ലിറ്റ്-സ്ക്രീനിനെ ആപ്പ് പിന്തുണയ്ക്കുന്നില്ല."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"രണ്ടാം ഡിസ്പ്ലേയിൽ ആപ്പ് പ്രവർത്തിച്ചേക്കില്ല."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"രണ്ടാം ഡിസ്പ്ലേകളിൽ സമാരംഭിക്കുന്നതിനെ ആപ്പ് അനുവദിക്കുന്നില്ല."</string> + <string name="accessibility_divider" msgid="703810061635792791">"സ്പ്ലിറ്റ്-സ്ക്രീൻ ഡിവൈഡർ"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ഇടത് പൂർണ്ണ സ്ക്രീൻ"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ഇടത് 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ഇടത് 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ഇടത് 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"വലത് പൂർണ്ണ സ്ക്രീൻ"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"മുകളിൽ പൂർണ്ണ സ്ക്രീൻ"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"മുകളിൽ 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"മുകളിൽ 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"മുകളിൽ 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"താഴെ പൂർണ്ണ സ്ക്രീൻ"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"ഒറ്റക്കൈ മോഡ് എങ്ങനെ ഉപയോഗിക്കാം"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"പുറത്ത് കടക്കാൻ, സ്ക്രീനിന്റെ ചുവടെ നിന്ന് മുകളിലേക്ക് സ്വൈപ്പ് ചെയ്യുക അല്ലെങ്കിൽ ആപ്പിന് മുകളിലായി എവിടെയെങ്കിലും ടാപ്പ് ചെയ്യുക"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"ഒറ്റക്കൈ മോഡ് ആരംഭിച്ചു"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"ഒറ്റക്കൈ മോഡിൽ നിന്ന് പുറത്തുകടക്കുക"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> ബബിളുകളുടെ ക്രമീകരണം"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"ഓവർഫ്ലോ"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"അടുക്കുകളിലേക്ക് തിരിച്ച് ചേർക്കുക"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g>-ൽ നിന്നുള്ള <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> എന്നതിൽ നിന്നുള്ള <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>, <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> കൂടുതലും"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"മുകളിൽ ഇടതുഭാഗത്തേക്ക് നീക്കുക"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"മുകളിൽ വലതുഭാഗത്തേക്ക് നീക്കുക"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"ചുവടെ ഇടതുഭാഗത്തേക്ക് നീക്കുക"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ചുവടെ വലതുഭാഗത്തേക്ക് നീക്കുക"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ക്രമീകരണം"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"ബബിൾ ഡിസ്മിസ് ചെയ്യൂ"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"സംഭാഷണം ബബിൾ ചെയ്യരുത്"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"ബബിളുകൾ ഉപയോഗിച്ച് ചാറ്റ് ചെയ്യുക"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"പുതിയ സംഭാഷണങ്ങൾ ഫ്ലോട്ടിംഗ് ഐക്കണുകളോ ബബിളുകളോ ആയി ദൃശ്യമാവുന്നു. ബബിൾ തുറക്കാൻ ടാപ്പ് ചെയ്യൂ. ഇത് നീക്കാൻ വലിച്ചിടുക."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"ബബിളുകൾ ഏതുസമയത്തും നിയന്ത്രിക്കുക"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"ഈ ആപ്പിൽ നിന്നുള്ള ബബിളുകൾ ഓഫാക്കാൻ മാനേജ് ചെയ്യുക ടാപ്പ് ചെയ്യുക"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"മനസ്സിലായി"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"അടുത്തിടെയുള്ള ബബിളുകൾ ഒന്നുമില്ല"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"അടുത്തിടെയുള്ള ബബിളുകൾ, ഡിസ്മിസ് ചെയ്ത ബബിളുകൾ എന്നിവ ഇവിടെ ദൃശ്യമാവും"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"ബബ്ൾ"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"മാനേജ് ചെയ്യുക"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ബബ്ൾ ഡിസ്മിസ് ചെയ്തു."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ml/strings_tv.xml b/libs/WindowManager/Shell/res/values-ml/strings_tv.xml new file mode 100644 index 000000000000..c74e0bbfaa5b --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ml/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ചിത്രത്തിനുള്ളിൽ ചിത്രം"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(പേരില്ലാത്ത പ്രോഗ്രാം)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP അടയ്ക്കുക"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"പൂര്ണ്ണ സ്ക്രീന്"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-mn/strings.xml b/libs/WindowManager/Shell/res/values-mn/strings.xml new file mode 100644 index 000000000000..044fd9fa7544 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-mn/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Хаах"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Дэлгэх"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Тохиргоо"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Цэс"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> дэлгэцэн доторх дэлгэцэд байна"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Та <xliff:g id="NAME">%s</xliff:g>-д энэ онцлогийг ашиглуулахыг хүсэхгүй байвал тохиргоог нээгээд, үүнийг унтраана уу."</string> + <string name="pip_play" msgid="3496151081459417097">"Тоглуулах"</string> + <string name="pip_pause" msgid="690688849510295232">"Түр зогсоох"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Дараагийн медиад очих"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Өмнөх медиад очих"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Хэмжээг өөрчлөх"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Апп хуваагдсан дэлгэц дээр ажиллахгүй байж болзошгүй."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Энэ апп нь дэлгэц хуваах тохиргоог дэмждэггүй."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Апп хоёрдогч дэлгэцэд ажиллахгүй."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Аппыг хоёрдогч дэлгэцэд эхлүүлэх боломжгүй."</string> + <string name="accessibility_divider" msgid="703810061635792791">"\"Дэлгэц хуваах\" хуваагч"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Зүүн талын бүтэн дэлгэц"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Зүүн 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Зүүн 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Зүүн 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Баруун талын бүтэн дэлгэц"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Дээд талын бүтэн дэлгэц"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Дээд 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Дээд 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Дээд 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Доод бүтэн дэлгэц"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Нэг гарын горимыг ашиглаж байна"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Гарахын тулд дэлгэцийн доод хэсгээс дээш шударч эсвэл апп дээр хүссэн газраа товшино уу"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Нэг гарын горимыг эхлүүлэх"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Нэг гарын горимоос гарах"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g>-н бөмбөлгүүдийн тохиргоо"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Халих"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Өрөлтөд буцааж нэмэх"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g>-н <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g>-н <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> болон бусад <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Зүүн дээш зөөх"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Баруун дээш зөөх"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Зүүн доош зөөх"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Баруун доош зөөх"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>-н тохиргоо"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Бөмбөлгийг хаах"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Харилцан яриаг бүү бөмбөлөг болго"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Бөмбөлөг ашиглан чатлаарай"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Шинэ харилцан яриа нь хөвөгч дүрс тэмдэг эсвэл бөмбөлөг хэлбэрээр харагддаг. Бөмбөлгийг нээхийн тулд товшино уу. Түүнийг зөөхийн тулд чирнэ үү."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Дурын үед бөмбөлгийг хянаарай"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Энэ аппын бөмбөлгүүдийг унтраахын тулд Удирдах дээр товшино уу"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Ойлголоо"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Саяхны бөмбөлөг алга байна"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Саяхны бөмбөлгүүд болон үл хэрэгссэн бөмбөлгүүд энд харагдана"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Бөмбөлөг"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Удирдах"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Бөмбөлгийг үл хэрэгссэн."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-mn/strings_tv.xml b/libs/WindowManager/Shell/res/values-mn/strings_tv.xml new file mode 100644 index 000000000000..55519d462b69 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-mn/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Дэлгэц доторх дэлгэц"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Гарчиггүй хөтөлбөр)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP-г хаах"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Бүтэн дэлгэц"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-mr/strings.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml new file mode 100644 index 000000000000..e838cf59331e --- /dev/null +++ b/libs/WindowManager/Shell/res/values-mr/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"बंद करा"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"विस्तृत करा"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"सेटिंग्ज"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"मेनू"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> चित्रामध्ये चित्र मध्ये आहे"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g>ने हे वैशिष्ट्य वापरू नये असे तुम्हाला वाटत असल्यास, सेटिंग्ज उघडण्यासाठी टॅप करा आणि ते बंद करा."</string> + <string name="pip_play" msgid="3496151081459417097">"प्ले करा"</string> + <string name="pip_pause" msgid="690688849510295232">"थांबवा"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"डावलून पुढे जा"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"डावलून मागे जा"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"आकार बदला"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"अॅप कदाचित स्प्लिट स्क्रीनसह काम करू शकत नाही."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"अॅप स्क्रीन-विभाजनास समर्थन देत नाही."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"दुसऱ्या डिस्प्लेवर अॅप कदाचित चालणार नाही."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"दुसऱ्या डिस्प्लेवर अॅप लाँच होणार नाही."</string> + <string name="accessibility_divider" msgid="703810061635792791">"विभाजित-स्क्रीन विभाजक"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"डावी फुल स्क्रीन"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"डावी 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"डावी 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"डावी 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"उजवी फुल स्क्रीन"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"शीर्ष फुल स्क्रीन"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"शीर्ष 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"शीर्ष 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"शीर्ष 10"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"तळाशी फुल स्क्रीन"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"एकहाती मोड वापरणे"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"बाहेर पडण्यासाठी स्क्रीनच्या खालून वरच्या दिशेने स्वाइप करा किंवा ॲपवर कोठेही टॅप करा"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"एकहाती मोड सुरू करा"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"एकहाती मोडमधून बाहेर पडा"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> बबलसाठी सेटिंग्ज"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"ओव्हरफ्लो"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"स्टॅकमध्ये परत जोडा"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g> कडून <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> आणि आणखी <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> कडून <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"वर डावीकडे हलवा"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"वर उजवीकडे हलवा"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"तळाशी डावीकडे हलवा"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"तळाशी उजवीकडे हलवा"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> सेटिंग्ज"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"बबल डिसमिस करा"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"संभाषणाला बबल करू नका"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"बबल वापरून चॅट करा"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"नवीन संभाषणे फ्लोटिंग आयकन किंवा बबल म्हणून दिसतात. बबल उघडण्यासाठी टॅप करा. हे हलवण्यासाठी ड्रॅग करा."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"बबल कधीही नियंत्रित करा"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"या अॅपमधून बबल बंद करण्यासाठी व्यवस्थापित करा वर टॅप करा"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"समजले"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"अलीकडील कोणतेही बबल नाहीत"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"अलीकडील बबल आणि डिसमिस केलेले बबल येथे दिसतील"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"बबल"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"व्यवस्थापित करा"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"बबल डिसमिस केला."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-mr/strings_tv.xml b/libs/WindowManager/Shell/res/values-mr/strings_tv.xml new file mode 100644 index 000000000000..ad2cfc6035c2 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-mr/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"चित्रात-चित्र"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(शीर्षक नसलेला कार्यक्रम)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP बंद करा"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"फुल स्क्रीन"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ms/strings.xml b/libs/WindowManager/Shell/res/values-ms/strings.xml new file mode 100644 index 000000000000..6664f38f3879 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ms/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Tutup"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Kembangkan"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Tetapan"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> terdapat dalam gambar dalam gambar"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Jika anda tidak mahu <xliff:g id="NAME">%s</xliff:g> menggunakan ciri ini, ketik untuk membuka tetapan dan matikan ciri."</string> + <string name="pip_play" msgid="3496151081459417097">"Main"</string> + <string name="pip_pause" msgid="690688849510295232">"Jeda"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Langkau ke seterusnya"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Langkau ke sebelumnya"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ubah saiz"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Apl mungkin tidak berfungsi dengan skrin pisah."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Apl tidak menyokong skrin pisah."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Apl mungkin tidak berfungsi pada paparan kedua."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Apl tidak menyokong pelancaran pada paparan kedua."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Pembahagi skrin pisah"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Skrin penuh kiri"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Kiri 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Kiri 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Kiri 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Skrin penuh kanan"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Skrin penuh atas"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Atas 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Atas 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Atas 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Skrin penuh bawah"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Menggunakan mod sebelah tangan"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Untuk keluar, leret ke atas daripada bahagian bawah skrin atau ketik pada mana-mana di bahagian atas apl"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Mulakan mod sebelah tangan"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Keluar daripada mod sebelah tangan"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Tetapan untuk gelembung <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Limpahan"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Tambah kembali pada tindanan"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> daripada <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> daripada <xliff:g id="APP_NAME">%2$s</xliff:g> dan <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> lagi"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Alihkan ke atas sebelah kiri"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Alihkan ke atas sebelah kanan"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Alihkan ke bawah sebelah kiri"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Alihkan ke bawah sebelah kanan"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Tetapan <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ketepikan gelembung"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Jangan jadikan perbualan dalam bentuk gelembung"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Bersembang menggunakan gelembung"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Perbualan baharu muncul sebagai ikon terapung atau gelembung. Ketik untuk membuka gelembung. Seret untuk mengalihkan gelembung tersebut."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Kawal gelembung pada bila-bila masa"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Ketik Urus untuk mematikan gelembung daripada apl ini"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Tiada gelembung terbaharu"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Gelembung baharu dan gelembung yang diketepikan akan dipaparkan di sini"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Gelembung"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Urus"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Gelembung diketepikan."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ms/strings_tv.xml b/libs/WindowManager/Shell/res/values-ms/strings_tv.xml new file mode 100644 index 000000000000..b2d7214381ef --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ms/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Gambar dalam Gambar"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program tiada tajuk)"</string> + <string name="pip_close" msgid="9135220303720555525">"Tutup PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Skrin penuh"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-my/strings.xml b/libs/WindowManager/Shell/res/values-my/strings.xml new file mode 100644 index 000000000000..9681d14a6a88 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-my/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"ပိတ်ရန်"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"ချဲ့ရန်"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"ဆက်တင်များ"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"မီနူး"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> သည် တစ်ခုပေါ် တစ်ခုထပ်၍ ဖွင့်ထားသည်"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> အား ဤဝန်ဆောင်မှုကို အသုံးမပြုစေလိုလျှင် ဆက်တင်ကိုဖွင့်ရန် တို့ပြီး ၎င်းဝန်ဆောင်မှုကို ပိတ်လိုက်ပါ။"</string> + <string name="pip_play" msgid="3496151081459417097">"ဖွင့်ရန်"</string> + <string name="pip_pause" msgid="690688849510295232">"ခေတ္တရပ်ရန်"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"နောက်တစ်ခုသို့ ကျော်ရန်"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"ယခင်တစ်ခုသို့ ပြန်သွားရန်"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"အရွယ်အစားပြောင်းရန်"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"မျက်နှာပြင် ခွဲ၍ပြသခြင်းဖြင့် အက်ပ်သည် အလုပ်မလုပ်ပါ။"</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"အက်ပ်သည် မျက်နှာပြင်ခွဲပြရန် ပံ့ပိုးထားခြင်းမရှိပါ။"</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ဤအက်ပ်အနေဖြင့် ဒုတိယဖန်သားပြင်ပေါ်တွင် အလုပ်လုပ်မည် မဟုတ်ပါ။"</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ဤအက်ပ်အနေဖြင့် ဖွင့်ရန်စနစ်ကို ဒုတိယဖန်သားပြင်မှ အသုံးပြုရန် ပံ့ပိုးမထားပါ။"</string> + <string name="accessibility_divider" msgid="703810061635792791">"မျက်နှာပြင်ခွဲခြမ်း ပိုင်းခြားပေးသည့်စနစ်"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ဘယ်ဘက် မျက်နှာပြင်အပြည့်"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ဘယ်ဘက်မျက်နှာပြင် ၇၀%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ဘယ်ဘက် မျက်နှာပြင် ၅၀%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ဘယ်ဘက် မျက်နှာပြင် ၃၀%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"ညာဘက် မျက်နှာပြင်အပြည့်"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"အပေါ်ဘက် မျက်နှာပြင်အပြည့်"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"အပေါ်ဘက် မျက်နှာပြင် ၇၀%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"အပေါ်ဘက် မျက်နှာပြင် ၅၀%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"အပေါ်ဘက် မျက်နှာပြင် ၃၀%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"အောက်ခြေ မျက်နှာပြင်အပြည့်"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"လက်တစ်ဖက်သုံးမုဒ် အသုံးပြုခြင်း"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"ထွက်ရန် ဖန်သားပြင်၏အောက်ခြေမှ အပေါ်သို့ပွတ်ဆွဲပါ သို့မဟုတ် အက်ပ်အပေါ်ဘက် မည်သည့်နေရာတွင်မဆို တို့ပါ"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"လက်တစ်ဖက်သုံးမုဒ်ကို စတင်လိုက်သည်"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"လက်တစ်ဖက်သုံးမုဒ်မှ ထွက်လိုက်သည်"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> ပူဖောင်းကွက်အတွက် ဆက်တင်များ"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"အပိုများပြရန်"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"ပူဖေါင်းတန်းသို့ ပြန်ထည့်ရန်"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g> မှ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> နှင့် နောက်ထပ် <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> ခုမှ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"ဘယ်ဘက်ထိပ်သို့ ရွှေ့ရန်"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ညာဘက်ထိပ်သို့ ရွှေ့ပါ"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"ဘယ်အောက်ခြေသို့ ရွှေ့ရန်"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ညာအောက်ခြေသို့ ရွှေ့ပါ"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ဆက်တင်များ"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"ပူဖောင်းကွက် ပယ်ရန်"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"စကားဝိုင်းကို ပူဖောင်းကွက် မပြုလုပ်ပါနှင့်"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"ပူဖောင်းကွက် သုံး၍ ချတ်လုပ်ခြင်း"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"စကားဝိုင်းအသစ်များကို မျောနေသည့် သင်္ကေတများ သို့မဟုတ် ပူဖောင်းကွက်များအဖြစ် မြင်ရပါမည်။ ပူဖောင်းကွက်ကိုဖွင့်ရန် တို့ပါ။ ရွှေ့ရန် ၎င်းကို ဖိဆွဲပါ။"</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"ပူဖောင်းကွက်ကို အချိန်မရွေး ထိန်းချုပ်ရန်"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"ဤအက်ပ်မှနေ၍ ပူဖောင်းများကို ပိတ်ရန်အတွက် \'စီမံရန်\' ကို တို့ပါ"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"ရပြီ"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"လတ်တလော ပူဖောင်းကွက်များ မရှိပါ"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"လတ်တလော ပူဖောင်းကွက်များနှင့် ပိတ်လိုက်သော ပူဖောင်းကွက်များကို ဤနေရာတွင် မြင်ရပါမည်"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"ပူဖောင်းဖောက်သံ"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"စီမံရန်"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ပူဖောင်းကွက် ဖယ်လိုက်သည်။"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-my/strings_tv.xml b/libs/WindowManager/Shell/res/values-my/strings_tv.xml new file mode 100644 index 000000000000..9569dc4cbeea --- /dev/null +++ b/libs/WindowManager/Shell/res/values-my/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"တစ်ခုပေါ်တစ်ခုထပ်၍ ဖွင့်ခြင်း"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ခေါင်းစဉ်မဲ့ အစီအစဉ်)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP ကိုပိတ်ပါ"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"မျက်နှာပြင် အပြည့်"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml new file mode 100644 index 000000000000..986e890dfe3a --- /dev/null +++ b/libs/WindowManager/Shell/res/values-nb/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Lukk"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Vis"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Innstillinger"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Meny"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> er i bilde-i-bilde"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Hvis du ikke vil at <xliff:g id="NAME">%s</xliff:g> skal bruke denne funksjonen, kan du trykke for å åpne innstillingene og slå den av."</string> + <string name="pip_play" msgid="3496151081459417097">"Spill av"</string> + <string name="pip_pause" msgid="690688849510295232">"Sett på pause"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Hopp til neste"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Hopp til forrige"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Endre størrelse"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Det kan hende at appen ikke fungerer med delt skjerm."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Appen støtter ikke delt skjerm."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Appen fungerer kanskje ikke på en sekundær skjerm."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Appen kan ikke kjøres på sekundære skjermer."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Skilleelement for delt skjerm"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Utvid den venstre delen av skjermen til hele skjermen"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Sett størrelsen på den venstre delen av skjermen til 70 %"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Sett størrelsen på den venstre delen av skjermen til 50 %"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Sett størrelsen på den venstre delen av skjermen til 30 %"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Utvid den høyre delen av skjermen til hele skjermen"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Utvid den øverste delen av skjermen til hele skjermen"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Sett størrelsen på den øverste delen av skjermen til 70 %"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Sett størrelsen på den øverste delen av skjermen til 50 %"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Sett størrelsen på den øverste delen av skjermen til 30 %"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Utvid den nederste delen av skjermen til hele skjermen"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Bruk av enhåndsmodus"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"For å avslutte, sveip opp fra bunnen av skjermen eller trykk hvor som helst over appen"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Start enhåndsmodus"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Avslutt enhåndsmodus"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Innstillinger for <xliff:g id="APP_NAME">%1$s</xliff:g>-bobler"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Overflyt"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Legg tilbake i stabelen"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> fra <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> fra <xliff:g id="APP_NAME">%2$s</xliff:g> og <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> flere"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Flytt til øverst til venstre"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Flytt til øverst til høyre"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Flytt til nederst til venstre"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Flytt til nederst til høyre"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>-innstillinger"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Lukk boblen"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ikke vis samtaler i bobler"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat med bobler"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nye samtaler vises som flytende ikoner eller bobler. Trykk for å åpne bobler. Dra for å flytte dem."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Kontrollér bobler når som helst"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Trykk på Administrer for å slå av bobler for denne appen"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Greit"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Ingen nylige bobler"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Nylige bobler og avviste bobler vises her"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Boble"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Administrer"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Boblen er avvist."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-nb/strings_tv.xml b/libs/WindowManager/Shell/res/values-nb/strings_tv.xml new file mode 100644 index 000000000000..8a7f315606ad --- /dev/null +++ b/libs/WindowManager/Shell/res/values-nb/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Bilde-i-bilde"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program uten tittel)"</string> + <string name="pip_close" msgid="9135220303720555525">"Lukk PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Fullskjerm"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml new file mode 100644 index 000000000000..0369c6dd2831 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ne/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"बन्द गर्नुहोस्"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"विस्तृत गर्नुहोस्"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"सेटिङहरू"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"मेनु"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> Picture-in-picture मा छ"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"तपाईं <xliff:g id="NAME">%s</xliff:g> ले सुविधा प्रयोग नगरोस् भन्ने चाहनुहुन्छ भने ट्याप गरेर सेटिङहरू खोल्नुहोस् र यसलाई निष्क्रिय पार्नुहोस्।"</string> + <string name="pip_play" msgid="3496151081459417097">"प्ले गर्नुहोस्"</string> + <string name="pip_pause" msgid="690688849510295232">"पज गर्नुहोस्"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"अर्कोमा जानुहोस्"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"अघिल्लोमा जानुहोस्"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"आकार बदल्नुहोस्"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"एप विभाजित स्क्रिनमा काम नगर्न सक्छ।"</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"अनुप्रयोगले विभाजित-स्क्रिनलाई समर्थन गर्दैन।"</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"यो अनुप्रयोगले सहायक प्रदर्शनमा काम नगर्नसक्छ।"</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"अनुप्रयोगले सहायक प्रदर्शनहरूमा लञ्च सुविधालाई समर्थन गर्दैन।"</string> + <string name="accessibility_divider" msgid="703810061635792791">"विभाजित-स्क्रिन छुट्याउने"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"बायाँ भाग फुल स्क्रिन"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"बायाँ भाग ७०%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"बायाँ भाग ५०%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"बायाँ भाग ३०%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"दायाँ भाग फुल स्क्रिन"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"माथिल्लो भाग फुल स्क्रिन"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"माथिल्लो भाग ७०%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"माथिल्लो भाग ५०%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"माथिल्लो भाग ३०%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"तल्लो भाग फुल स्क्रिन"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"एक हाते मोड प्रयोग गरिँदै छ"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"बाहिर निस्कन, स्क्रिनको पुछारबाट माथितिर स्वाइप गर्नुहोस् वा एपभन्दा माथि जुनसुकै ठाउँमा ट्याप गर्नुहोस्"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"एक हाते मोड सुरु गर्नुहोस्"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"एक हाते मोडबाट बाहिरिनुहोस्"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> का बबलसम्बन्धी सेटिङहरू"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"ओभरफ्लो देखाउनुहोस्"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"स्ट्याकमा फेरि थप्नुहोस्"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g> को <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> का <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> र थप <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"शीर्ष भागको बायाँतिर सार्नुहोस्"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"सिरानमा दायाँतिर सार्नुहोस्"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"पुछारमा बायाँतिर सार्नुहोस्"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"पुछारमा दायाँतिर सार्नुहोस्"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> का सेटिङहरू"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"बबल खारेज गर्नुहोस्"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"वार्तालाप बबलको रूपमा नदेखाइयोस्"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"बबलहरू प्रयोग गरी कुराकानी गर्नुहोस्"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"नयाँ वार्तालापहरू तैरने आइकन वा बबलका रूपमा देखिन्छन्। बबल खोल्न ट्याप गर्नुहोस्। बबल सार्न सो बबललाई ड्र्याग गर्नुहोस्।"</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"जुनसुकै बेला बबलहरू नियन्त्रण गर्नुहोस्"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"यो एपबाट आएका बबलहरू अफ गर्न \"व्यवस्थापन गर्नुहोस्\" बटनमा ट्याप गर्नुहोस्"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"बुझेँ"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"हालैका बबलहरू छैनन्"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"हालैका बबल र खारेज गरिएका बबलहरू यहाँ देखिने छन्"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"बबल"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"व्यवस्थापन गर्नुहोस्"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"बबल हटाइयो।"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ne/strings_tv.xml b/libs/WindowManager/Shell/res/values-ne/strings_tv.xml new file mode 100644 index 000000000000..87fa3279f05e --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ne/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-Picture"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(शीर्षकविहीन कार्यक्रम)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP लाई बन्द गर्नुहोस्"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"फुल स्क्रिन"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-night/colors.xml b/libs/WindowManager/Shell/res/values-night/colors.xml new file mode 100644 index 000000000000..83c4d93982f4 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-night/colors.xml @@ -0,0 +1,22 @@ +<!-- + ~ 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> + <!-- Bubbles --> + <color name="bubbles_icon_tint">@color/GM2_grey_200</color> + <!-- Splash screen--> + <color name="splash_window_background_default">@color/splash_screen_bg_dark</color> +</resources>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml new file mode 100644 index 000000000000..26c276e7e690 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-nl/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Sluiten"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Uitvouwen"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Instellingen"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in scherm-in-scherm"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Als je niet wilt dat <xliff:g id="NAME">%s</xliff:g> deze functie gebruikt, tik je om de instellingen te openen en schakel je de functie uit."</string> + <string name="pip_play" msgid="3496151081459417097">"Afspelen"</string> + <string name="pip_pause" msgid="690688849510295232">"Onderbreken"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Doorgaan naar volgende"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Teruggaan naar vorige"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Formaat aanpassen"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"De app werkt mogelijk niet met gesplitst scherm."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App biedt geen ondersteuning voor gesplitst scherm."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App werkt mogelijk niet op een secundair scherm."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App kan niet op secundaire displays worden gestart."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Scheiding voor gesplitst scherm"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Linkerscherm op volledig scherm"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Linkerscherm 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Linkerscherm 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Linkerscherm 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Rechterscherm op volledig scherm"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Bovenste scherm op volledig scherm"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Bovenste scherm 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Bovenste scherm 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Bovenste scherm 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Onderste scherm op volledig scherm"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Bediening met één hand gebruiken"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Als je wilt afsluiten, swipe je omhoog vanaf de onderkant van het scherm of tik je ergens boven de app"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Bediening met één hand starten"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Bediening met één hand afsluiten"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Instellingen voor <xliff:g id="APP_NAME">%1$s</xliff:g>-bubbels"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Overloop"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Weer toevoegen aan stack"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> van <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> van <xliff:g id="APP_NAME">%2$s</xliff:g> en nog <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Naar linksboven verplaatsen"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Naar rechtsboven verplaatsen"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Naar linksonder verplaatsen"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Naar rechtsonder verplaatsen"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Instellingen voor <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Bubbel sluiten"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Gesprekken niet in bubbels weergeven"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatten met bubbels"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nieuwe gesprekken worden weergegeven als zwevende iconen of \'bubbels\'. Tik om een bubbel te openen. Sleep om de bubbel te verplaatsen."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Beheer bubbels wanneer je wilt"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Tik op Beheren om bubbels van deze app uit te schakelen"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Geen recente bubbels"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Recente bubbels en gesloten bubbels worden hier weergegeven"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Bubbel"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Beheren"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubbel gesloten."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-nl/strings_tv.xml b/libs/WindowManager/Shell/res/values-nl/strings_tv.xml new file mode 100644 index 000000000000..df3809e5d6c6 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-nl/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Scherm-in-scherm"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Naamloos programma)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP sluiten"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Volledig scherm"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-or/strings.xml b/libs/WindowManager/Shell/res/values-or/strings.xml new file mode 100644 index 000000000000..27f16226a421 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-or/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"ବନ୍ଦ କରନ୍ତୁ"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"ବଢ଼ାନ୍ତୁ"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"ସେଟିଂସ୍"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"ମେନୁ"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> \"ଛବି-ଭିତରେ-ଛବି\"ରେ ଅଛି"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"ଏହି ବୈଶିଷ୍ଟ୍ୟ <xliff:g id="NAME">%s</xliff:g> ବ୍ୟବହାର ନକରିବାକୁ ଯଦି ଆପଣ ଚାହାଁନ୍ତି, ସେଟିଙ୍ଗ ଖୋଲିବାକୁ ଟାପ୍ କରନ୍ତୁ ଏବଂ ଏହା ଅଫ୍ କରିଦିଅନ୍ତୁ।"</string> + <string name="pip_play" msgid="3496151081459417097">"ପ୍ଲେ କରନ୍ତୁ"</string> + <string name="pip_pause" msgid="690688849510295232">"ପଜ୍ କରନ୍ତୁ"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"ପରବର୍ତ୍ତୀକୁ ଯାଆନ୍ତୁ"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"ପୂର୍ବବର୍ତ୍ତୀକୁ ଛାଡ଼ନ୍ତୁ"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ରିସାଇଜ୍ କରନ୍ତୁ"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"ସ୍ପ୍ଲିଟ୍-ସ୍କ୍ରିନରେ ଆପ୍ କାମ କରିନପାରେ।"</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ଆପ୍ ସ୍ପ୍ଲିଟ୍-ସ୍କ୍ରୀନକୁ ସପୋର୍ଟ କରେ ନାହିଁ।"</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ସେକେଣ୍ଡାରୀ ଡିସପ୍ଲେରେ ଆପ୍ କାମ ନକରିପାରେ।"</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ସେକେଣ୍ଡାରୀ ଡିସପ୍ଲେରେ ଆପ୍ ଲଞ୍ଚ ସପୋର୍ଟ କରେ ନାହିଁ।"</string> + <string name="accessibility_divider" msgid="703810061635792791">"ସ୍ପ୍ଲିଟ୍-ସ୍କ୍ରୀନ ବିଭାଜକ"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ବାମ ପଟକୁ ପୂର୍ଣ୍ଣ ସ୍କ୍ରୀନ୍ କରନ୍ତୁ"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ବାମ ପଟକୁ 70% କରନ୍ତୁ"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ବାମ ପଟକୁ 50% କରନ୍ତୁ"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ବାମ ପଟେ 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"ଡାହାଣ ପଟକୁ ପୂର୍ଣ୍ଣ ସ୍କ୍ରୀନ୍ କରନ୍ତୁ"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"ଉପର ଆଡ଼କୁ ପୂର୍ଣ୍ଣ ସ୍କ୍ରୀନ୍ କରନ୍ତୁ"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ଉପର ଆଡ଼କୁ 70% କରନ୍ତୁ"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ଉପର ଆଡ଼କୁ 50% କରନ୍ତୁ"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ଉପର ଆଡ଼କୁ 30% କରନ୍ତୁ"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"ତଳ ଅଂଶର ପୂର୍ଣ୍ଣ ସ୍କ୍ରୀନ୍"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"ଏକ-ହାତ ମୋଡ୍ ବ୍ୟବହାର କରି"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"ବାହାରି ଯିବା ପାଇଁ, ସ୍କ୍ରିନର ତଳୁ ଉପରକୁ ସ୍ୱାଇପ୍ କରନ୍ତୁ କିମ୍ବା ଆପରେ ଯେ କୌଣସି ସ୍ଥାନରେ ଟାପ୍ କରନ୍ତୁ"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"ଏକ-ହାତ ମୋଡ୍ ଆରମ୍ଭ କରନ୍ତୁ"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"ଏକ-ହାତ ମୋଡରୁ ବାହାରି ଯାଆନ୍ତୁ"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> ବବଲ୍ଗୁଡ଼ିକ ପାଇଁ ସେଟିଂସ୍"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"ଓଭରଫ୍ଲୋ"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"ଷ୍ଟାକରେ ପୁଣି ଯୋଗ କରନ୍ତୁ"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g>ରୁ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> ଏବଂ ଅଧିକ <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>ଟିରୁ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"ଉପର ବାମକୁ ନିଅନ୍ତୁ"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ଉପର-ଡାହାଣକୁ ନିଅନ୍ତୁ"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"ତଳ ବାମକୁ ନିଅନ୍ତୁ"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ତଳ ଡାହାଣକୁ ନିଅନ୍ତୁ"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ସେଟିଂସ୍"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"ବବଲ୍ ଖାରଜ କରନ୍ତୁ"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"ବାର୍ତ୍ତାଳାପକୁ ବବଲ୍ କରନ୍ତୁ ନାହିଁ"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"ବବଲଗୁଡ଼ିକୁ ବ୍ୟବହାର କରି ଚାଟ୍ କରନ୍ତୁ"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"ନୂଆ ବାର୍ତ୍ତାଳାପଗୁଡ଼ିକ ଫ୍ଲୋଟିଂ ଆଇକନ୍ କିମ୍ବା ବବଲ୍ ଭାବେ ଦେଖାଯିବ। ବବଲ୍ ଖୋଲିବାକୁ ଟାପ୍ କରନ୍ତୁ। ଏହାକୁ ମୁଭ୍ କରିବାକୁ ଟାଣନ୍ତୁ।"</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"ଯେ କୌଣସି ସମୟରେ ବବଲଗୁଡ଼ିକ ନିୟନ୍ତ୍ରଣ କରନ୍ତୁ"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"ଏହି ଆପର ବବଲଗୁଡ଼ିକ ବନ୍ଦ କରିବା ପାଇଁ \'ପରିଚାଳନା କରନ୍ତୁ\' ବଟନରେ ଟାପ୍ କରନ୍ତୁ"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"ବୁଝିଗଲି"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"ବର୍ତ୍ତମାନ କୌଣସି ବବଲ୍ ନାହିଁ"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"ବର୍ତ୍ତମାନର ଏବଂ ଖାରଜ କରାଯାଇଥିବା ବବଲଗୁଡ଼ିକ ଏଠାରେ ଦେଖାଯିବ"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"ବବଲ୍"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"ପରିଚାଳନା କରନ୍ତୁ"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ବବଲ୍ ଖାରଜ କରାଯାଇଛି।"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-or/strings_tv.xml b/libs/WindowManager/Shell/res/values-or/strings_tv.xml new file mode 100644 index 000000000000..295a5c4ee1ce --- /dev/null +++ b/libs/WindowManager/Shell/res/values-or/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ପିକଚର୍-ଇନ୍-ପିକଚର୍"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(କୌଣସି ଟାଇଟଲ୍ ପ୍ରୋଗ୍ରାମ୍ ନାହିଁ)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP ବନ୍ଦ କରନ୍ତୁ"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"ପୂର୍ଣ୍ଣ ସ୍କ୍ରୀନ୍"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml new file mode 100644 index 000000000000..96688b952d66 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-pa/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"ਬੰਦ ਕਰੋ"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"ਵਿਸਤਾਰ ਕਰੋ"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"ਸੈਟਿੰਗਾਂ"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"ਮੀਨੂ"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ਤਸਵੀਰ-ਅੰਦਰ-ਤਸਵੀਰ ਵਿੱਚ ਹੈ"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"ਜੇਕਰ ਤੁਸੀਂ ਨਹੀਂ ਚਾਹੁੰਦੇ ਕਿ <xliff:g id="NAME">%s</xliff:g> ਐਪ ਇਸ ਵਿਸ਼ੇਸ਼ਤਾ ਦੀ ਵਰਤੋਂ ਕਰੇ, ਤਾਂ ਸੈਟਿੰਗਾਂ ਖੋਲ੍ਹਣ ਲਈ ਟੈਪ ਕਰੋ ਅਤੇ ਇਸਨੂੰ ਬੰਦ ਕਰੋ।"</string> + <string name="pip_play" msgid="3496151081459417097">"ਚਲਾਓ"</string> + <string name="pip_pause" msgid="690688849510295232">"ਵਿਰਾਮ ਦਿਓ"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"ਅਗਲੇ \'ਤੇ ਜਾਓ"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"ਪਿਛਲੇ \'ਤੇ ਜਾਓ"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ਆਕਾਰ ਬਦਲੋ"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"ਹੋ ਸਕਦਾ ਹੈ ਕਿ ਐਪ ਸਪਲਿਟ-ਸਕ੍ਰੀਨ ਨਾਲ ਕੰਮ ਨਾ ਕਰੇ।"</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ਐਪ ਸਪਲਿਟ-ਸਕ੍ਰੀਨ ਨੂੰ ਸਮਰਥਨ ਨਹੀਂ ਕਰਦੀ।"</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ਹੋ ਸਕਦਾ ਹੈ ਕਿ ਐਪ ਸੈਕੰਡਰੀ ਡਿਸਪਲੇ \'ਤੇ ਕੰਮ ਨਾ ਕਰੇ।"</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ਐਪ ਸੈਕੰਡਰੀ ਡਿਸਪਲੇਆਂ \'ਤੇ ਲਾਂਚ ਕਰਨ ਦਾ ਸਮਰਥਨ ਨਹੀਂ ਕਰਦੀ"</string> + <string name="accessibility_divider" msgid="703810061635792791">"ਸਪਲਿਟ-ਸਕ੍ਰੀਨ ਡਿਵਾਈਡਰ"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ਖੱਬੇ ਪੂਰੀ ਸਕ੍ਰੀਨ"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ਖੱਬੇ 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ਖੱਬੇ 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ਖੱਬੇ 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"ਸੱਜੇ ਪੂਰੀ ਸਕ੍ਰੀਨ"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"ਉੱਪਰ ਪੂਰੀ ਸਕ੍ਰੀਨ"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ਉੱਪਰ 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ਉੱਪਰ 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ਉੱਪਰ 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"ਹੇਠਾਂ ਪੂਰੀ ਸਕ੍ਰੀਨ"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"ਇੱਕ ਹੱਥ ਮੋਡ ਵਰਤਣਾ"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"ਬਾਹਰ ਜਾਣ ਲਈ, ਸਕ੍ਰੀਨ ਦੇ ਹੇਠਾਂ ਤੋਂ ਉੱਪਰ ਵੱਲ ਸਵਾਈਪ ਕਰੋ ਜਾਂ ਐਪ \'ਤੇ ਕਿਤੇ ਵੀ ਟੈਪ ਕਰੋ"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"ਇੱਕ ਹੱਥ ਮੋਡ ਸ਼ੁਰੂ ਕਰੋ"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"ਇੱਕ ਹੱਥ ਮੋਡ ਤੋਂ ਬਾਹਰ ਜਾਓ"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> ਬਬਲ ਲਈ ਸੈਟਿੰਗਾਂ"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"ਓਵਰਫ਼ਲੋ"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"ਸਟੈਕ ਵਿੱਚ ਵਾਪਸ ਸ਼ਾਮਲ ਕਰੋ"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g> ਤੋਂ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> ਅਤੇ <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> ਹੋਰਾਂ ਤੋਂ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"ਉੱਪਰ ਵੱਲ ਖੱਬੇ ਲਿਜਾਓ"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ਉੱਪਰ ਵੱਲ ਸੱਜੇ ਲਿਜਾਓ"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"ਹੇਠਾਂ ਵੱਲ ਖੱਬੇ ਲਿਜਾਓ"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ਹੇਠਾਂ ਵੱਲ ਸੱਜੇ ਲਿਜਾਓ"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ਸੈਟਿੰਗਾਂ"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"ਬਬਲ ਨੂੰ ਖਾਰਜ ਕਰੋ"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"ਗੱਲਬਾਤ \'ਤੇ ਬਬਲ ਨਾ ਲਾਓ"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"ਬਬਲ ਵਰਤਦੇ ਹੋਏ ਚੈਟ ਕਰੋ"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"ਨਵੀਆਂ ਗੱਲਾਂਬਾਤਾਂ ਫਲੋਟਿੰਗ ਪ੍ਰਤੀਕਾਂ ਜਾਂ ਬਬਲ ਦੇ ਰੂਪ ਵਿੱਚ ਦਿਸਦੀਆਂ ਹਨ। ਬਬਲ ਨੂੰ ਖੋਲ੍ਹਣ ਲਈ ਟੈਪ ਕਰੋ। ਇਸਨੂੰ ਲਿਜਾਣ ਲਈ ਘਸੀਟੋ।"</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"ਬਬਲ ਨੂੰ ਕਿਸੇ ਵੇਲੇ ਵੀ ਕੰਟਰੋਲ ਕਰੋ"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"ਇਸ ਐਪ \'ਤੇ ਬਬਲ ਬੰਦ ਕਰਨ ਲਈ \'ਪ੍ਰਬੰਧਨ ਕਰੋ\' \'ਤੇ ਟੈਪ ਕਰੋ"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"ਸਮਝ ਲਿਆ"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"ਕੋਈ ਹਾਲੀਆ ਬਬਲ ਨਹੀਂ"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"ਹਾਲੀਆ ਬਬਲ ਅਤੇ ਖਾਰਜ ਕੀਤੇ ਬਬਲ ਇੱਥੇ ਦਿਸਣਗੇ"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"ਬੁਲਬੁਲਾ"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"ਪ੍ਰਬੰਧਨ ਕਰੋ"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ਬਬਲ ਨੂੰ ਖਾਰਜ ਕੀਤਾ ਗਿਆ।"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-pa/strings_tv.xml b/libs/WindowManager/Shell/res/values-pa/strings_tv.xml new file mode 100644 index 000000000000..e32895a9a239 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-pa/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"ਤਸਵੀਰ-ਵਿੱਚ-ਤਸਵੀਰ"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ਸਿਰਲੇਖ-ਰਹਿਤ ਪ੍ਰੋਗਰਾਮ)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP ਬੰਦ ਕਰੋ"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"ਪੂਰੀ ਸਕ੍ਰੀਨ"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml new file mode 100644 index 000000000000..6b640b54f898 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-pl/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Zamknij"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Rozwiń"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Ustawienia"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"Aplikacja <xliff:g id="NAME">%s</xliff:g> działa w trybie obraz w obrazie"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Jeśli nie chcesz, by aplikacja <xliff:g id="NAME">%s</xliff:g> korzystała z tej funkcji, otwórz ustawienia i wyłącz ją."</string> + <string name="pip_play" msgid="3496151081459417097">"Odtwórz"</string> + <string name="pip_pause" msgid="690688849510295232">"Wstrzymaj"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Dalej"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Wstecz"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Zmień rozmiar"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacja może nie działać przy podzielonym ekranie."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacja nie obsługuje dzielonego ekranu."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacja może nie działać na dodatkowym ekranie."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacja nie obsługuje uruchamiania na dodatkowych ekranach."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Linia dzielenia ekranu"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Lewa część ekranu na pełnym ekranie"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70% lewej części ekranu"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50% lewej części ekranu"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"30% lewej części ekranu"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Prawa część ekranu na pełnym ekranie"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Górna część ekranu na pełnym ekranie"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"70% górnej części ekranu"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50% górnej części ekranu"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"30% górnej części ekranu"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Dolna część ekranu na pełnym ekranie"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Korzystanie z trybu jednej ręki"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Aby zamknąć, przesuń palcem z dołu ekranu w górę lub kliknij dowolne miejsce nad aplikacją"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Uruchom tryb jednej ręki"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Zamknij tryb jednej ręki"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Ustawienia dymków aplikacji <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Przepełnienie"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Dodaj ponownie do stosu"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> z aplikacji <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> z aplikacji <xliff:g id="APP_NAME">%2$s</xliff:g> i jeszcze <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Przenieś w lewy górny róg"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Przenieś w prawy górny róg"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Przenieś w lewy dolny róg"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Przenieś w prawy dolny róg"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> – ustawienia"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Zamknij dymek"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nie wyświetlaj rozmowy jako dymka"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Czatuj, korzystając z dymków"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nowe rozmowy będą wyświetlane jako pływające ikony lub dymki. Kliknij, by otworzyć dymek. Przeciągnij, by go przenieść."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Zarządzaj dymkami w dowolnym momencie"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Kliknij Zarządzaj, aby wyłączyć dymki z tej aplikacji"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Brak ostatnich dymków"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Tutaj będą pojawiać się ostatnie i odrzucone dymki"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Dymek"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Zarządzaj"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Zamknięto dymek"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-pl/strings_tv.xml b/libs/WindowManager/Shell/res/values-pl/strings_tv.xml new file mode 100644 index 000000000000..286fd7b2ff0f --- /dev/null +++ b/libs/WindowManager/Shell/res/values-pl/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Obraz w obrazie"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez tytułu)"</string> + <string name="pip_close" msgid="9135220303720555525">"Zamknij PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Pełny ekran"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml new file mode 100644 index 000000000000..465d2d17a5e7 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Fechar"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Expandir"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Configurações"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está em picture-in-picture"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Se você não quer que o app <xliff:g id="NAME">%s</xliff:g> use este recurso, toque para abrir as configurações e desativá-lo."</string> + <string name="pip_play" msgid="3496151081459417097">"Reproduzir"</string> + <string name="pip_pause" msgid="690688849510295232">"Pausar"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Pular para a próxima"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Pular para a anterior"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionar"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"É possível que o app não funcione com a tela dividida."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"O app não é compatível com a divisão de tela."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"É possível que o app não funcione em uma tela secundária."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"O app não é compatível com a inicialização em telas secundárias."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Divisor de tela"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Lado esquerdo em tela cheia"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Esquerda a 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Esquerda a 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Esquerda a 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Lado direito em tela cheia"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Parte superior em tela cheia"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Parte superior a 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Parte superior a 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Parte superior a 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Parte inferior em tela cheia"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Como usar o modo para uma mão"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Para sair, deslize de baixo para cima na tela ou toque em qualquer lugar acima do app"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Iniciar o modo para uma mão"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Sair do modo para uma mão"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Configurações de balões do <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Menu flutuante"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Devolver à pilha"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g> mais <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mover para canto superior esquerdo"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover para canto superior direito"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mover para canto inferior esquerdo"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover para canto inferior direito"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Configurações de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dispensar balão"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Não criar balões de conversa"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Converse usando balões"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Novas conversas aparecerão como ícones flutuantes, ou balões. Toque para abrir o balão. Arraste para movê-lo."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Controle os balões a qualquer momento"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Toque em \"Gerenciar\" para desativar os balões desse app"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Ok"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Nenhum balão recente"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Os balões recentes e dispensados aparecerão aqui"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Bolha"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Gerenciar"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balão dispensado."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml new file mode 100644 index 000000000000..57edcdf74cf4 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(programa sem título)"</string> + <string name="pip_close" msgid="9135220303720555525">"Fechar PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Tela cheia"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml new file mode 100644 index 000000000000..df841bf3eda4 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Fechar"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Expandir"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Definições"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"A app <xliff:g id="NAME">%s</xliff:g> está no modo de ecrã no ecrã"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Se não pretende que a app <xliff:g id="NAME">%s</xliff:g> utilize esta funcionalidade, toque para abrir as definições e desative-a."</string> + <string name="pip_play" msgid="3496151081459417097">"Reproduzir"</string> + <string name="pip_pause" msgid="690688849510295232">"Pausar"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Mudar para o seguinte"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Mudar para o anterior"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionar"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"A app pode não funcionar com o ecrã dividido."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"A app não é compatível com o ecrã dividido."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"A app pode não funcionar num ecrã secundário."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"A app não é compatível com o início em ecrãs secundários."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Divisor do ecrã dividido"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ecrã esquerdo inteiro"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70% no ecrã esquerdo"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50% no ecrã esquerdo"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"30% no ecrã esquerdo"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Ecrã direito inteiro"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Ecrã superior inteiro"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"70% no ecrã superior"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50% no ecrã superior"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"30% no ecrã superior"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Ecrã inferior inteiro"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Utilize o modo para uma mão"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Para sair, deslize rapidamente para cima a partir da parte inferior do ecrã ou toque em qualquer ponto acima da app."</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Iniciar o modo para uma mão"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Sair do modo para uma mão"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Definições dos balões da app <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Menu adicional"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Adicionar novamente à pilha"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> do <xliff:g id="APP_NAME">%2$s</xliff:g> e mais<xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>."</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mover p/ parte sup. esquerda"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover parte superior direita"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mover p/ parte infer. esquerda"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover parte inferior direita"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Definições de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ignorar balão"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Não apresentar a conversa em balões"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Converse no chat através de balões"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"As novas conversas aparecem como ícones flutuantes ou balões. Toque para abrir o balão. Arraste para o mover."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Controle os balões em qualquer altura"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Toque em Gerir para desativar os balões desta app."</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Nenhum balão recente"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Os balões recentes e ignorados vão aparecer aqui."</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Balão"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Gerir"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balão ignorado."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml new file mode 100644 index 000000000000..9372e0f637cb --- /dev/null +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Ecrã no ecrã"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Sem título do programa)"</string> + <string name="pip_close" msgid="9135220303720555525">"Fechar PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Ecrã inteiro"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-pt/strings.xml b/libs/WindowManager/Shell/res/values-pt/strings.xml new file mode 100644 index 000000000000..465d2d17a5e7 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-pt/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Fechar"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Expandir"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Configurações"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está em picture-in-picture"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Se você não quer que o app <xliff:g id="NAME">%s</xliff:g> use este recurso, toque para abrir as configurações e desativá-lo."</string> + <string name="pip_play" msgid="3496151081459417097">"Reproduzir"</string> + <string name="pip_pause" msgid="690688849510295232">"Pausar"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Pular para a próxima"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Pular para a anterior"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionar"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"É possível que o app não funcione com a tela dividida."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"O app não é compatível com a divisão de tela."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"É possível que o app não funcione em uma tela secundária."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"O app não é compatível com a inicialização em telas secundárias."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Divisor de tela"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Lado esquerdo em tela cheia"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Esquerda a 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Esquerda a 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Esquerda a 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Lado direito em tela cheia"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Parte superior em tela cheia"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Parte superior a 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Parte superior a 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Parte superior a 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Parte inferior em tela cheia"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Como usar o modo para uma mão"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Para sair, deslize de baixo para cima na tela ou toque em qualquer lugar acima do app"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Iniciar o modo para uma mão"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Sair do modo para uma mão"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Configurações de balões do <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Menu flutuante"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Devolver à pilha"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g> mais <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mover para canto superior esquerdo"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover para canto superior direito"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mover para canto inferior esquerdo"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover para canto inferior direito"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Configurações de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dispensar balão"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Não criar balões de conversa"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Converse usando balões"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Novas conversas aparecerão como ícones flutuantes, ou balões. Toque para abrir o balão. Arraste para movê-lo."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Controle os balões a qualquer momento"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Toque em \"Gerenciar\" para desativar os balões desse app"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Ok"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Nenhum balão recente"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Os balões recentes e dispensados aparecerão aqui"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Bolha"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Gerenciar"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balão dispensado."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-pt/strings_tv.xml b/libs/WindowManager/Shell/res/values-pt/strings_tv.xml new file mode 100644 index 000000000000..57edcdf74cf4 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-pt/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(programa sem título)"</string> + <string name="pip_close" msgid="9135220303720555525">"Fechar PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Tela cheia"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ro/strings.xml b/libs/WindowManager/Shell/res/values-ro/strings.xml new file mode 100644 index 000000000000..55a437668b22 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ro/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Închideți"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Extindeți"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Setări"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Meniu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> este în modul picture-in-picture"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Dacă nu doriți ca <xliff:g id="NAME">%s</xliff:g> să utilizeze această funcție, atingeți pentru a deschide setările și dezactivați-o."</string> + <string name="pip_play" msgid="3496151081459417097">"Redați"</string> + <string name="pip_pause" msgid="690688849510295232">"Întrerupeți"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Treceți la următorul"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Treceți la cel anterior"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionați"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Este posibil ca aplicația să nu funcționeze cu ecranul împărțit."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplicația nu acceptă ecranul împărțit."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Este posibil ca aplicația să nu funcționeze pe un ecran secundar."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplicația nu acceptă lansare pe ecrane secundare."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Separator pentru ecranul împărțit"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Partea stângă pe ecran complet"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Partea stângă: 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Partea stângă: 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Partea stângă: 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Partea dreaptă pe ecran complet"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Partea de sus pe ecran complet"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Partea de sus: 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Partea de sus: 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Partea de sus: 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Partea de jos pe ecran complet"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Folosirea modului cu o mână"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Pentru a ieși, glisați în sus din partea de jos a ecranului sau atingeți oriunde deasupra ferestrei aplicației"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Activați modul cu o mână"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Părăsiți modul cu o mână"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Setări pentru baloanele <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Suplimentar"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Adăugați înapoi în stivă"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de la <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de la <xliff:g id="APP_NAME">%2$s</xliff:g> și încă <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mutați în stânga sus"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mutați în dreapta sus"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mutați în stânga jos"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mutați în dreapta jos"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Setări <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Închideți balonul"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nu afișați conversația în balon"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat cu baloane"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Conversațiile noi apar ca pictograme flotante sau baloane. Atingeți pentru a deschide balonul. Trageți pentru a-l muta."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Controlați oricând baloanele"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Atingeți Gestionați pentru a dezactiva baloanele din această aplicație"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Nu există baloane recente"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Baloanele recente și baloanele respinse vor apărea aici"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Balon"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestionați"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balonul a fost respins."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ro/strings_tv.xml b/libs/WindowManager/Shell/res/values-ro/strings_tv.xml new file mode 100644 index 000000000000..9438e4955b68 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ro/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-picture"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program fără titlu)"</string> + <string name="pip_close" msgid="9135220303720555525">"Închideți PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Ecran complet"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml new file mode 100644 index 000000000000..8ae00d28f896 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ru/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Закрыть"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Развернуть"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Настройки"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Меню"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> находится в режиме \"Картинка в картинке\""</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Чтобы отключить эту функцию для приложения \"<xliff:g id="NAME">%s</xliff:g>\", перейдите в настройки."</string> + <string name="pip_play" msgid="3496151081459417097">"Воспроизвести"</string> + <string name="pip_pause" msgid="690688849510295232">"Приостановить"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Перейти к следующему"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Перейти к предыдущему"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Изменить размер"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"В режиме разделения экрана приложение может работать нестабильно."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Приложение не поддерживает разделение экрана."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Приложение может не работать на дополнительном экране"</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Приложение не поддерживает запуск на дополнительных экранах"</string> + <string name="accessibility_divider" msgid="703810061635792791">"Разделитель экрана"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Левый во весь экран"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Левый на 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Левый на 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Левый на 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Правый во весь экран"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Верхний во весь экран"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Верхний на 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Верхний на 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Верхний на 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Нижний во весь экран"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Использование режима управления одной рукой"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Чтобы выйти, проведите по экрану снизу вверх или коснитесь области за пределами приложения."</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Запустить режим управления одной рукой"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Выйти из режима управления одной рукой"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Настройки всплывающих чатов от приложения \"<xliff:g id="APP_NAME">%1$s</xliff:g>\"."</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Дополнительное меню"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Добавить обратно в стек"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> из приложения \"<xliff:g id="APP_NAME">%2$s</xliff:g>\""</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> от приложения \"<xliff:g id="APP_NAME">%2$s</xliff:g>\" и ещё <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Перенести в левый верхний угол"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Перенести в правый верхний угол"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Перенести в левый нижний угол"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Перенести в правый нижний угол"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>: настройки"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Скрыть всплывающий чат"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Не показывать всплывающий чат для разговора"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Всплывающие чаты"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Новые разговоры будут появляться в виде плавающих значков, или всплывающих чатов. Чтобы открыть чат, нажмите на него, а чтобы переместить – перетащите."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Всплывающие чаты"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Чтобы отключить всплывающие чаты из этого приложения, нажмите \"Настроить\"."</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"ОК"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Нет недавних всплывающих чатов"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Здесь будут появляться недавние и скрытые всплывающие чаты."</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Всплывающая подсказка"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Настроить"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Всплывающий чат закрыт."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ru/strings_tv.xml b/libs/WindowManager/Shell/res/values-ru/strings_tv.xml new file mode 100644 index 000000000000..24785aa7e184 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ru/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Картинка в картинке"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Без названия)"</string> + <string name="pip_close" msgid="9135220303720555525">"\"Кадр в кадре\" – выйти"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Во весь экран"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-si/strings.xml b/libs/WindowManager/Shell/res/values-si/strings.xml new file mode 100644 index 000000000000..081926fd101b --- /dev/null +++ b/libs/WindowManager/Shell/res/values-si/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"වසන්න"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"දිග හරින්න"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"සැකසීම්"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"මෙනුව"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> පින්තූරය-තුළ-පින්තූරය තුළ වේ"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"ඔබට <xliff:g id="NAME">%s</xliff:g> මෙම විශේෂාංගය භාවිත කිරීමට අවශ්ය නැති නම්, සැකසීම් විවෘත කිරීමට තට්ටු කර එය ක්රියාවිරහිත කරන්න."</string> + <string name="pip_play" msgid="3496151081459417097">"ධාවනය කරන්න"</string> + <string name="pip_pause" msgid="690688849510295232">"විරාම කරන්න"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"ඊළඟ එකට පනින්න"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"පෙර එකට පනින්න"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ප්රතිප්රමාණ කරන්න"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"යෙදුම බෙදුම් තිරය සමග ක්රියා නොකළ හැකිය"</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"යෙදුම බෙදුණු-තිරය සඳහා සහාය නොදක්වයි."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"යෙදුම ද්විතියික සංදර්ශකයක ක්රියා නොකළ හැකිය."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"යෙදුම ද්විතීයික සංදර්ශක මත දියත් කිරීම සඳහා සහාය නොදක්වයි."</string> + <string name="accessibility_divider" msgid="703810061635792791">"බෙදුම්-තිර වෙන්කරණය"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"වම් පූර්ණ තිරය"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"වම් 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"වම් 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"වම් 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"දකුණු පූර්ණ තිරය"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"ඉහළම පූර්ණ තිරය"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ඉහළම 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ඉහළම 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ඉහළම 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"පහළ පූර්ණ තිරය"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"තනි-අත් ප්රකාරය භාවිත කරමින්"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"පිටවීමට, තිරයේ පහළ සිට ඉහළට ස්වයිප් කරන්න හෝ යෙදුමට ඉහළින් ඕනෑම තැනක තට්ටු කරන්න"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"තනි අත් ප්රකාරය ආරම්භ කරන්න"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"තනි අත් ප්රකාරයෙන් පිටවන්න"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> බුබුළු සඳහා සැකසීම්"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"පිටාර යාම"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"අට්ටිය වෙත ආපසු එක් කරන්න"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g> වෙතින් <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> වෙතින් <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> සහ තවත් <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> ක්"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"ඉහළ වමට ගෙන යන්න"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ඉහළ දකුණට ගෙන යන්න"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"පහළ වමට ගෙන යන්න"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"පහළ දකුණට ගෙන යන්න"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> සැකසීම්"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"බුබුලු ඉවත ලන්න"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"සංවාදය බුබුලු නොදමන්න"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"බුබුලු භාවිතයෙන් කතාබහ කරන්න"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"නව සංවාද පාවෙන අයිකන හෝ බුබුලු ලෙස දිස් වේ. බුබුල විවෘත කිරීමට තට්ටු කරන්න. එය ගෙන යාමට අදින්න."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"ඕනෑම වේලාවක බුබුලු පාලනය කරන්න"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"මෙම යෙදුමෙන් බුබුලු ක්රියාවිරහිත කිරීමට කළමනාකරණය කරන්න තට්ටු කරන්න"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"තේරුණා"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"මෑත බුබුලු නැත"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"මෑත බුබුලු සහ ඉවත ලූ බුබුලු මෙහි දිස් වනු ඇත"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"බුබුළු"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"කළමනා කරන්න"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"බුබුල ඉවත දමා ඇත."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-si/strings_tv.xml b/libs/WindowManager/Shell/res/values-si/strings_tv.xml new file mode 100644 index 000000000000..62ee6d4f44d2 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-si/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"පින්තූරය-තුළ-පින්තූරය"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(මාතෘකාවක් නැති වැඩසටහන)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP වසන්න"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"සම්පූර්ණ තිරය"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-sk/strings.xml b/libs/WindowManager/Shell/res/values-sk/strings.xml new file mode 100644 index 000000000000..24fded7ebb04 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-sk/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Zavrieť"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Rozbaliť"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Nastavenia"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Ponuka"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> je v režime obraz v obraze"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Ak nechcete, aby aplikácia <xliff:g id="NAME">%s</xliff:g> používala túto funkciu, klepnutím otvorte nastavenia a vypnite ju."</string> + <string name="pip_play" msgid="3496151081459417097">"Prehrať"</string> + <string name="pip_pause" msgid="690688849510295232">"Pozastaviť"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Preskočiť na ďalšie"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Preskočiť na predchádzajúce"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Zmeniť veľkosť"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikácia nemusí fungovať s rozdelenou obrazovkou."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikácia nepodporuje rozdelenú obrazovku."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikácia nemusí fungovať na sekundárnej obrazovke."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikácia nepodporuje spúšťanie na sekundárnych obrazovkách."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Rozdeľovač obrazovky"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ľavá – na celú obrazovku"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Ľavá – 70 %"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Ľavá – 50 %"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Ľavá – 30 %"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Pravá– na celú obrazovku"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Horná – na celú obrazovku"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Horná – 70 %"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Horná – 50 %"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Horná – 30 %"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Dolná – na celú obrazovku"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Používanie režimu jednej ruky"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Ukončíte potiahnutím z dolnej časti obrazovky nahor alebo klepnutím kdekoľvek nad aplikáciu"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Spustiť režim jednej ruky"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Ukončiť režim jednej ruky"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Nastavenia bublín aplikácie <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Rozšírená ponuka"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Pridať späť do zásobníka"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> z aplikácie <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> z aplikácie <xliff:g id="APP_NAME">%2$s</xliff:g> a ďalšie (<xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>)"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Presunúť doľava nahor"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Presunúť doprava nahor"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Presunúť doľava nadol"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Presunúť doprava nadol"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Nastavenia aplikácie <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Zavrieť bublinu"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nezobrazovať konverzáciu ako bublinu"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Čet pomocou bublín"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nové konverzácie sa zobrazujú ako plávajúce ikony či bubliny. Bublinu otvoríte klepnutím. Premiestnite ju presunutím."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Nastavenie bublín môžete kedykoľvek zmeniť"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Bubliny pre túto aplikáciu môžete vypnúť klepnutím na Spravovať"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Dobre"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Žiadne nedávne bubliny"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Tu sa budú zobrazovať nedávne a zavreté bubliny"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Bublina"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Spravovať"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bublina bola zavretá."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-sk/strings_tv.xml b/libs/WindowManager/Shell/res/values-sk/strings_tv.xml new file mode 100644 index 000000000000..a7a515cdc61c --- /dev/null +++ b/libs/WindowManager/Shell/res/values-sk/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Obraz v obraze"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez názvu)"</string> + <string name="pip_close" msgid="9135220303720555525">"Zavrieť režim PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Celá obrazovka"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml new file mode 100644 index 000000000000..3f425302a5ac --- /dev/null +++ b/libs/WindowManager/Shell/res/values-sl/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Zapri"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Razširi"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Nastavitve"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Meni"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> je v načinu slika v sliki"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Če ne želite, da aplikacija <xliff:g id="NAME">%s</xliff:g> uporablja to funkcijo, se dotaknite, da odprete nastavitve, in funkcijo izklopite."</string> + <string name="pip_play" msgid="3496151081459417097">"Predvajaj"</string> + <string name="pip_pause" msgid="690688849510295232">"Začasno ustavi"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Preskoči na naslednjega"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Preskoči na prejšnjega"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Spremeni velikost"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacija morda ne deluje v načinu razdeljenega zaslona."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podpira načina razdeljenega zaslona."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija morda ne bo delovala na sekundarnem zaslonu."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacija ne podpira zagona na sekundarnih zaslonih."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Razdelilnik zaslonov"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Levi v celozaslonski način"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Levi 70 %"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Levi 50 %"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Levi 30 %"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Desni v celozaslonski način"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Zgornji v celozaslonski način"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Zgornji 70 %"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Zgornji 50 %"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Zgornji 30 %"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Spodnji v celozaslonski način"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Uporaba enoročnega načina"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Za izhod povlecite z dna zaslona navzgor ali se dotaknite na poljubnem mestu nad aplikacijo"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Zagon enoročnega načina"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Izhod iz enoročnega načina"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Nastavitve za oblačke aplikacije <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Prelivanje"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Dodaj nazaj v sklad"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> (<xliff:g id="APP_NAME">%2$s</xliff:g>)"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> iz aplikacije <xliff:g id="APP_NAME">%2$s</xliff:g> in toliko drugih: <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Premakni zgoraj levo"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Premakni zgoraj desno"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Premakni spodaj levo"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Premakni spodaj desno"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Nastavitve za <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Opusti oblaček"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Pogovora ne prikaži v oblačku"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Klepet z oblački"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Novi pogovori so prikazani kot lebdeče ikone ali oblački. Če želite odpreti oblaček, se ga dotaknite. Če ga želite premakniti, ga povlecite."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Upravljanje oblačkov"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Dotaknite se »Upravljanje«, da izklopite oblačke iz te aplikacije"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"V redu"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Ni nedavnih oblačkov"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Tukaj bodo prikazani tako nedavni kot tudi opuščeni oblački"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Mehurček"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljanje"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblaček je bil opuščen."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-sl/strings_tv.xml b/libs/WindowManager/Shell/res/values-sl/strings_tv.xml new file mode 100644 index 000000000000..fe5c9ae5d2a8 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-sl/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Slika v sliki"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program brez naslova)"</string> + <string name="pip_close" msgid="9135220303720555525">"Zapri način PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Celozaslonsko"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-sq/strings.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml new file mode 100644 index 000000000000..ddae724e7569 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-sq/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Mbyll"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Zgjero"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Cilësimet"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menyja"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> është në figurë brenda figurës"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Nëse nuk dëshiron që <xliff:g id="NAME">%s</xliff:g> ta përdorë këtë funksion, trokit për të hapur cilësimet dhe për ta çaktivizuar."</string> + <string name="pip_play" msgid="3496151081459417097">"Luaj"</string> + <string name="pip_pause" msgid="690688849510295232">"Ndërprit"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Kalo te tjetra"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Kalo tek e mëparshmja"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ndrysho përmasat"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacioni mund të mos funksionojë me ekranin e ndarë."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacioni nuk mbështet ekranin e ndarë."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacioni mund të mos funksionojë në një ekran dytësor."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacioni nuk mbështet nisjen në ekrane dytësore."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Ndarësi i ekranit të ndarë"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ekrani i plotë majtas"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Majtas 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Majtas 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Majtas 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Ekrani i plotë djathtas"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Ekrani i plotë lart"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Lart 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Lart 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Lart 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Ekrani i plotë poshtë"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Po përdor modalitetin e përdorimit me një dorë"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Për të dalë, rrëshqit lart nga fundi i ekranit ose trokit diku mbi aplikacion"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Modaliteti i përdorimit me një dorë"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Dil nga modaliteti i përdorimit me një dorë"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Cilësimet për flluskat e <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Tejkalo"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Shto përsëri te stiva"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> nga <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> nga <xliff:g id="APP_NAME">%2$s</xliff:g> dhe <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> të tjera"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Zhvendos lart majtas"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Lëviz lart djathtas"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Zhvendos poshtë majtas"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Lëvize poshtë djathtas"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Cilësimet e <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Hiqe flluskën"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Mos e vendos bisedën në flluskë"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Bisedo duke përdorur flluskat"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Bisedat e reja shfaqen si ikona pluskuese ose flluska. Trokit për të hapur flluskën. Zvarrit për ta zhvendosur."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Kontrollo flluskat në çdo moment"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Trokit \"Menaxho\" për të çaktivizuar flluskat nga ky aplikacion"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"E kuptova"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Nuk ka flluska të fundit"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Flluskat e fundit dhe flluskat e hequra do të shfaqen këtu"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Flluskë"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Menaxho"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Flluska u hoq."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-sq/strings_tv.xml b/libs/WindowManager/Shell/res/values-sq/strings_tv.xml new file mode 100644 index 000000000000..1d5583b2c826 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-sq/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Figurë brenda figurës"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program pa titull)"</string> + <string name="pip_close" msgid="9135220303720555525">"Mbyll PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Ekrani i plotë"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-sr/strings.xml b/libs/WindowManager/Shell/res/values-sr/strings.xml new file mode 100644 index 000000000000..74c9ac0867e3 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-sr/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Затвори"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Прошири"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Подешавања"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Мени"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> је слика у слици"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Ако не желите да <xliff:g id="NAME">%s</xliff:g> користи ову функцију, додирните да бисте отворили подешавања и искључили је."</string> + <string name="pip_play" msgid="3496151081459417097">"Пусти"</string> + <string name="pip_pause" msgid="690688849510295232">"Паузирај"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Пређи на следеће"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Пређи на претходно"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Промените величину"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Апликација можда неће радити са подељеним екраном."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Апликација не подржава подељени екран."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Апликација можда неће функционисати на секундарном екрану."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Апликација не подржава покретање на секундарним екранима."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Разделник подељеног екрана"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Режим целог екрана за леви екран"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Леви екран 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Леви екран 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Леви екран 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Режим целог екрана за доњи екран"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Режим целог екрана за горњи екран"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Горњи екран 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Горњи екран 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Горњи екран 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Режим целог екрана за доњи екран"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Коришћење режима једном руком"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Да бисте изашли, превуците нагоре од дна екрана или додирните било где изнад апликације"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Покрените режим једном руком"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Изађите из режима једном руком"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Подешавања за <xliff:g id="APP_NAME">%1$s</xliff:g> облачиће"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Преклапање"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Додај поново у групу"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> из апликације <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> из апликације <xliff:g id="APP_NAME">%2$s</xliff:g> и још <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Премести горе лево"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Премести горе десно"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Премести доле лево"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Премести доле десно"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Подешавања за <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Одбаци облачић"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Не користи облачиће за конверзацију"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Ћаскајте у облачићима"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Нове конверзације се приказују као плутајуће иконе или облачићи. Додирните да бисте отворили облачић. Превуците да бисте га преместили."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Контролишите облачиће у било ком тренутку"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Додирните Управљајте да бисте искључили облачиће из ове апликације"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Важи"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Нема недавних облачића"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Овде се приказују недавни и одбачени облачићи"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Облачић"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Управљајте"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Облачић је одбачен."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-sr/strings_tv.xml b/libs/WindowManager/Shell/res/values-sr/strings_tv.xml new file mode 100644 index 000000000000..62ad1e8f6e69 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-sr/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Слика у слици"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Програм без наслова)"</string> + <string name="pip_close" msgid="9135220303720555525">"Затвори PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Цео екран"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml new file mode 100644 index 000000000000..81328a836345 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-sv/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Stäng"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Utöka"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Inställningar"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Meny"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> visas i bild-i-bild"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Om du inte vill att den här funktionen används i <xliff:g id="NAME">%s</xliff:g> öppnar du inställningarna genom att trycka. Sedan inaktiverar du funktionen."</string> + <string name="pip_play" msgid="3496151081459417097">"Spela upp"</string> + <string name="pip_pause" msgid="690688849510295232">"Pausa"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Hoppa till nästa"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Hoppa till föregående"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ändra storlek"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Appen kanske inte fungerar med delad skärm."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Appen har inte stöd för delad skärm."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Appen kanske inte fungerar på en sekundär skärm."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Appen kan inte köras på en sekundär skärm."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Avdelare för delad skärm"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Helskärm på vänster skärm"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Vänster 70 %"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Vänster 50 %"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Vänster 30 %"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Helskärm på höger skärm"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Helskärm på övre skärm"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Övre 70 %"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Övre 50 %"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Övre 30 %"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Helskärm på nedre skärm"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Använda enhandsläge"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Avsluta genom att svepa uppåt från skärmens nederkant eller trycka ovanför appen"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Starta enhandsläge"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Avsluta enhandsläge"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Inställningar för <xliff:g id="APP_NAME">%1$s</xliff:g>-bubblor"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Fler menyalternativ"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Lägg tillbaka på stack"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> från <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> från <xliff:g id="APP_NAME">%2$s</xliff:g> och <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> fler"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Flytta högst upp till vänster"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Flytta högst upp till höger"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Flytta längst ned till vänster"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Flytta längst ned till höger"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Inställningar för <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Stäng bubbla"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Visa inte konversationen i bubblor"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatta med bubblor"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nya konversationer visas som flytande ikoner, så kallade bubblor. Tryck på bubblan om du vill öppna den. Dra den om du vill flytta den."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Styr bubblor när som helst"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Tryck på Hantera för att stänga av bubblor från den här appen"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Inga nya bubblor"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"De senaste bubblorna och ignorerade bubblor visas här"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Bubbla"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Hantera"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubblan ignorerades."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-sv/strings_tv.xml b/libs/WindowManager/Shell/res/values-sv/strings_tv.xml new file mode 100644 index 000000000000..74fb590c3e4d --- /dev/null +++ b/libs/WindowManager/Shell/res/values-sv/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Bild-i-bild"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Namnlöst program)"</string> + <string name="pip_close" msgid="9135220303720555525">"Stäng PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Helskärm"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-sw/strings.xml b/libs/WindowManager/Shell/res/values-sw/strings.xml new file mode 100644 index 000000000000..4559832b1d85 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-sw/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Funga"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Panua"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Mipangilio"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menyu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> iko katika hali ya picha ndani ya picha nyingine"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Ikiwa hutaki <xliff:g id="NAME">%s</xliff:g> itumie kipengele hiki, gusa ili ufungue mipangilio na uizime."</string> + <string name="pip_play" msgid="3496151081459417097">"Cheza"</string> + <string name="pip_pause" msgid="690688849510295232">"Sitisha"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Ruka ufikie inayofuata"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Ruka ufikie iliyotangulia"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Badilisha ukubwa"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Huenda programu isifanye kazi kwenye skrini inayogawanywa."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Programu haiwezi kutumia skrini iliyogawanywa."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Huenda programu isifanye kazi kwenye dirisha lingine."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Programu hii haiwezi kufunguliwa kwenye madirisha mengine."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Kitenganishi cha skrini inayogawanywa"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Skrini nzima ya kushoto"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Kushoto 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Kushoto 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Kushoto 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Skrini nzima ya kulia"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Skrini nzima ya juu"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Juu 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Juu 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Juu 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Skrini nzima ya chini"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Kutumia hali ya kutumia kwa mkono mmoja"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Ili ufunge, telezesha kidole juu kutoka sehemu ya chini ya skrini au uguse mahali popote juu ya programu"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Anzisha hali ya kutumia kwa mkono mmoja"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Funga hali ya kutumia kwa mkono mmoja"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Mipangilio ya viputo vya <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Vipengee vya ziada"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Rejesha kwenye rafu"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> kutoka kwa <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> kutoka kwa <xliff:g id="APP_NAME">%2$s</xliff:g> na nyingine<xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Sogeza juu kushoto"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Sogeza juu kulia"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Sogeza chini kushoto"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Sogeza chini kulia"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Mipangilio ya <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ondoa kiputo"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Usiweke viputo kwenye mazungumzo"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Piga gumzo ukitumia viputo"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Mazungumzo mapya huonekena kama aikoni au viputo vinavyoelea. Gusa ili ufungue kiputo. Buruta ili ukisogeze."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Dhibiti viputo wakati wowote"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Gusa Dhibiti ili uzime viputo kwenye programu hii"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Nimeelewa"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Hakuna viputo vya hivi majuzi"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Viputo vya hivi karibuni na vile vilivyoondolewa vitaonekana hapa"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Kiputo"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Dhibiti"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Umeondoa kiputo."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-sw/strings_tv.xml b/libs/WindowManager/Shell/res/values-sw/strings_tv.xml new file mode 100644 index 000000000000..cf0d8a9b3910 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-sw/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pachika Picha Ndani ya Picha Nyingine"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Programu isiyo na jina)"</string> + <string name="pip_close" msgid="9135220303720555525">"Funga PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Skrini nzima"</string> +</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-ta/strings.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml new file mode 100644 index 000000000000..586ee94a1098 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ta/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"மூடு"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"விரி"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"அமைப்புகள்"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"மெனு"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> தற்போது பிக்ச்சர்-இன்-பிக்ச்சரில் உள்ளது"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> இந்த அம்சத்தைப் பயன்படுத்த வேண்டாம் என நினைத்தால் இங்கு தட்டி அமைப்புகளைத் திறந்து இதை முடக்கவும்."</string> + <string name="pip_play" msgid="3496151081459417097">"இயக்கு"</string> + <string name="pip_pause" msgid="690688849510295232">"இடைநிறுத்து"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"அடுத்ததற்குச் செல்"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"முந்தையதற்குச் செல்"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"அளவு மாற்று"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"திரைப் பிரிப்பு அம்சத்தில் ஆப்ஸ் செயல்படாமல் போகக்கூடும்."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"திரையைப் பிரிப்பதைப் ஆப்ஸ் ஆதரிக்கவில்லை."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"இரண்டாம்நிலைத் திரையில் ஆப்ஸ் வேலை செய்யாமல் போகக்கூடும்."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"இரண்டாம்நிலைத் திரைகளில் பயன்பாட்டைத் தொடங்க முடியாது."</string> + <string name="accessibility_divider" msgid="703810061635792791">"திரையைப் பிரிக்கும் பிரிப்பான்"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"இடது புறம் முழுத் திரை"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"இடது புறம் 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"இடது புறம் 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"இடது புறம் 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"வலது புறம் முழுத் திரை"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"மேற்புறம் முழுத் திரை"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"மேலே 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"மேலே 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"மேலே 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"கீழ்ப்புறம் முழுத் திரை"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"ஒற்றைக் கைப் பயன்முறையைப் பயன்படுத்துதல்"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"வெளியேற, திரையின் கீழிருந்து மேல்நோக்கி ஸ்வைப் செய்யவும் அல்லது ஆப்ஸுக்கு மேலே ஏதேனும் ஓர் இடத்தில் தட்டவும்"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"ஒற்றைக் கைப் பயன்முறையைத் தொடங்கும்"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"ஒற்றைக் கைப் பயன்முறையில் இருந்து வெளியேறும்"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> குமிழ்களுக்கான அமைப்புகள்"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"ஓவர்ஃப்லோ"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"மீண்டும் ஸ்டேக்கில் சேர்க்கவும்"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g> இலிருந்து <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> மற்றும் மேலும் <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> ஆப்ஸிலிருந்து வந்துள்ள அறிவிப்பு: <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"மேலே இடப்புறமாக நகர்த்து"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"மேலே வலப்புறமாக நகர்த்து"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"கீழே இடப்புறமாக நகர்த்து"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"கீழே வலதுபுறமாக நகர்த்து"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> அமைப்புகள்"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"குமிழை அகற்று"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"உரையாடலைக் குமிழாக்காதே"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"குமிழ்களைப் பயன்படுத்தி அரட்டையடியுங்கள்"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"புதிய உரையாடல்கள் மிதக்கும் ஐகான்களாகவோ குமிழ்களாகவோ தோன்றும். குமிழைத் திறக்க தட்டவும். நகர்த்த இழுக்கவும்."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"குமிழ்களை எப்போது வேண்டுமானாலும் கட்டுப்படுத்தலாம்"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"இந்த ஆப்ஸிலிருந்து வரும் குமிழ்களை முடக்க, நிர்வகி என்பதைத் தட்டவும்"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"சரி"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"சமீபத்திய குமிழ்கள் இல்லை"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"சமீபத்திய குமிழ்களும் நிராகரிக்கப்பட்ட குமிழ்களும் இங்கே தோன்றும்"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"பபிள்"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"நிர்வகி"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"குமிழ் நிராகரிக்கப்பட்டது."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ta/strings_tv.xml b/libs/WindowManager/Shell/res/values-ta/strings_tv.xml new file mode 100644 index 000000000000..8bca46314e30 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ta/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"பிக்ச்சர்-இன்-பிக்ச்சர்"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(தலைப்பு இல்லை)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIPஐ மூடு"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"முழுத்திரை"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-te/strings.xml b/libs/WindowManager/Shell/res/values-te/strings.xml new file mode 100644 index 000000000000..4e85b4371220 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-te/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"మూసివేయి"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"విస్తరింపజేయి"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"సెట్టింగ్లు"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"మెనూ"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> చిత్రంలో చిత్రం రూపంలో ఉంది"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ఈ లక్షణాన్ని ఉపయోగించకూడదు అని మీరు అనుకుంటే, సెట్టింగ్లను తెరవడానికి ట్యాప్ చేసి, దీన్ని ఆఫ్ చేయండి."</string> + <string name="pip_play" msgid="3496151081459417097">"ప్లే చేయి"</string> + <string name="pip_pause" msgid="690688849510295232">"పాజ్ చేయి"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"దాటవేసి తర్వాత దానికి వెళ్లు"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"దాటవేసి మునుపటి దానికి వెళ్లు"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"పరిమాణం మార్చు"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"స్క్రీన్ విభజనతో యాప్ పని చేయకపోవచ్చు."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"అనువర్తనంలో స్క్రీన్ విభజనకు మద్దతు లేదు."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ప్రత్యామ్నాయ డిస్ప్లేలో యాప్ పని చేయకపోవచ్చు."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ప్రత్యామ్నాయ డిస్ప్లేల్లో ప్రారంభానికి యాప్ మద్దతు లేదు."</string> + <string name="accessibility_divider" msgid="703810061635792791">"విభజన స్క్రీన్ విభాగిని"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ఎడమవైపు పూర్తి స్క్రీన్"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ఎడమవైపు 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ఎడమవైపు 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ఎడమవైపు 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"కుడివైపు పూర్తి స్క్రీన్"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"ఎగువ పూర్తి స్క్రీన్"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ఎగువ 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ఎగువ 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ఎగువ 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"దిగువ పూర్తి స్క్రీన్"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"వన్-హ్యాండెడ్ మోడ్ను ఉపయోగించడం"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"నిష్క్రమించడానికి, స్క్రీన్ కింది భాగం నుండి పైకి స్వైప్ చేయండి లేదా యాప్ పైన ఎక్కడైనా ట్యాప్ చేయండి"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"వన్-హ్యాండెడ్ మోడ్ను ప్రారంభిస్తుంది"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"వన్-హ్యాండెడ్ మోడ్ నుండి నిష్క్రమింపజేస్తుంది"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> బబుల్స్ సెట్టింగ్లు"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"ఓవర్ఫ్లో"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"స్ట్యాక్కు తిరిగి జోడించండి"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g> నుండి <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> నుండి <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> మరియు మరో <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"ఎగువ ఎడమవైపునకు జరుపు"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ఎగువ కుడివైపునకు జరుపు"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"దిగువ ఎడమవైపునకు తరలించు"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"దిగవు కుడివైపునకు జరుపు"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> సెట్టింగ్లు"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"బబుల్ను విస్మరించు"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"సంభాషణను బబుల్ చేయవద్దు"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"బబుల్స్ను ఉపయోగించి చాట్ చేయండి"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"కొత్త సంభాషణలు తేలియాడే చిహ్నాలుగా లేదా బబుల్స్ లాగా కనిపిస్తాయి. బబుల్ని తెరవడానికి నొక్కండి. తరలించడానికి లాగండి."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"బబుల్స్ను ఎప్పుడైనా నియంత్రించండి"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"ఈ యాప్ నుండి వచ్చే బబుల్స్ను ఆఫ్ చేయడానికి మేనేజ్ బటన్ను ట్యాప్ చేయండి"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"అర్థమైంది"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"ఇటీవలి బబుల్స్ ఏవీ లేవు"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"ఇటీవలి బబుల్స్ మరియు తీసివేసిన బబుల్స్ ఇక్కడ కనిపిస్తాయి"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"బబుల్"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"మేనేజ్ చేయండి"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"బబుల్ విస్మరించబడింది."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-te/strings_tv.xml b/libs/WindowManager/Shell/res/values-te/strings_tv.xml new file mode 100644 index 000000000000..47489efbc4c2 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-te/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"పిక్చర్-ఇన్-పిక్చర్"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(శీర్షిక లేని ప్రోగ్రామ్)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIPని మూసివేయి"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"పూర్తి స్క్రీన్"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-th/strings.xml b/libs/WindowManager/Shell/res/values-th/strings.xml new file mode 100644 index 000000000000..66c701812ce8 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-th/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"ปิด"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"ขยาย"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"การตั้งค่า"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"เมนู"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ใช้การแสดงภาพซ้อนภาพ"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"หากคุณไม่ต้องการให้ <xliff:g id="NAME">%s</xliff:g> ใช้ฟีเจอร์นี้ ให้แตะเพื่อเปิดการตั้งค่าแล้วปิดฟีเจอร์"</string> + <string name="pip_play" msgid="3496151081459417097">"เล่น"</string> + <string name="pip_pause" msgid="690688849510295232">"หยุดชั่วคราว"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"ข้ามไปรายการถัดไป"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"ข้ามไปรายการก่อนหน้า"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ปรับขนาด"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"แอปอาจใช้ไม่ได้กับโหมดแบ่งหน้าจอ"</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"แอปไม่สนับสนุนการแยกหน้าจอ"</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"แอปอาจไม่ทำงานในจอแสดงผลรอง"</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"แอปไม่รองรับการเรียกใช้ในจอแสดงผลรอง"</string> + <string name="accessibility_divider" msgid="703810061635792791">"เส้นแบ่งหน้าจอ"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"เต็มหน้าจอทางซ้าย"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ซ้าย 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ซ้าย 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ซ้าย 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"เต็มหน้าจอทางขวา"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"เต็มหน้าจอด้านบน"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ด้านบน 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ด้านบน 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ด้านบน 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"เต็มหน้าจอด้านล่าง"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"การใช้โหมดมือเดียว"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"หากต้องการออก ให้เลื่อนขึ้นจากด้านล่างของหน้าจอหรือแตะที่ใดก็ได้เหนือแอป"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"เริ่มโหมดมือเดียว"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"ออกจากโหมดมือเดียว"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"การตั้งค่าบับเบิล <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"รายการเพิ่มเติม"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"เพิ่มกลับไปที่สแต็ก"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> จาก <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> จาก <xliff:g id="APP_NAME">%2$s</xliff:g> และอีก <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> รายการ"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"ย้ายไปด้านซ้ายบน"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ย้ายไปด้านขวาบน"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"ย้ายไปด้านซ้ายล่าง"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ย้ายไปด้านขาวล่าง"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"การตั้งค่า <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"ปิดบับเบิล"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"ไม่ต้องแสดงการสนทนาเป็นบับเบิล"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"แชทโดยใช้บับเบิล"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"การสนทนาใหม่ๆ จะปรากฏเป็นไอคอนแบบลอยหรือบับเบิล แตะเพื่อเปิดบับเบิล ลากเพื่อย้ายที่"</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"ควบคุมบับเบิลได้ทุกเมื่อ"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"แตะ \"จัดการ\" เพื่อปิดบับเบิลจากแอปนี้"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"รับทราบ"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"ไม่มีบับเบิลเมื่อเร็วๆ นี้"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"บับเบิลที่แสดงและที่ปิดไปเมื่อเร็วๆ นี้จะปรากฏที่นี่"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"บับเบิล"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"จัดการ"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ปิดบับเบิลแล้ว"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-th/strings_tv.xml b/libs/WindowManager/Shell/res/values-th/strings_tv.xml new file mode 100644 index 000000000000..d3797e7c3cde --- /dev/null +++ b/libs/WindowManager/Shell/res/values-th/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"การแสดงภาพซ้อนภาพ"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ไม่มีชื่อรายการ)"</string> + <string name="pip_close" msgid="9135220303720555525">"ปิด PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"เต็มหน้าจอ"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-tl/strings.xml b/libs/WindowManager/Shell/res/values-tl/strings.xml new file mode 100644 index 000000000000..a76bf6f1350c --- /dev/null +++ b/libs/WindowManager/Shell/res/values-tl/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Isara"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Palawakin"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Mga Setting"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"Nasa picture-in-picture ang <xliff:g id="NAME">%s</xliff:g>"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Kung ayaw mong magamit ni <xliff:g id="NAME">%s</xliff:g> ang feature na ito, i-tap upang buksan ang mga setting at i-off ito."</string> + <string name="pip_play" msgid="3496151081459417097">"I-play"</string> + <string name="pip_pause" msgid="690688849510295232">"I-pause"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Lumaktaw sa susunod"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Lumaktaw sa nakaraan"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"I-resize"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Posibleng hindi gumana ang app sa split screen."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Hindi sinusuportahan ng app ang split-screen."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Maaaring hindi gumana ang app sa pangalawang display."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Hindi sinusuportahan ng app ang paglulunsad sa mga pangalawang display."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Divider ng split-screen"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"I-full screen ang nasa kaliwa"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Gawing 70% ang nasa kaliwa"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Gawing 50% ang nasa kaliwa"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Gawing 30% ang nasa kaliwa"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"I-full screen ang nasa kanan"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"I-full screen ang nasa itaas"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Gawing 70% ang nasa itaas"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Gawing 50% ang nasa itaas"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Gawing 30% ang nasa itaas"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"I-full screen ang nasa ibaba"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Paggamit ng one-hand mode"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Para lumabas, mag-swipe pataas mula sa ibaba ng screen o mag-tap kahit saan sa itaas ng app"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Simulan ang one-hand mode"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Lumabas sa one-hand mode"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Mga setting para sa mga bubble ng <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Overflow"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Idagdag ulit sa stack"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> mula sa <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> mula sa <xliff:g id="APP_NAME">%2$s</xliff:g> at <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> pa"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Ilipat sa kaliwa sa itaas"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Ilipat sa kanan sa itaas"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Ilipat sa kaliwa sa ibaba"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Ilipat sa kanan sa ibaba"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Mga setting ng <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"I-dismiss ang bubble"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Huwag ipakita sa bubble ang mga pag-uusap"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Mag-chat gamit ang bubbles"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Lumalabas bilang mga nakalutang na icon o bubble ang mga bagong pag-uusap. I-tap para buksan ang bubble. I-drag para ilipat ito."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Kontrolin ang mga bubble anumang oras"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"I-tap ang Pamahalaan para i-off ang mga bubble mula sa app na ito"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Walang kamakailang bubble"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Lalabas dito ang mga kamakailang bubble at na-dismiss na bubble"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Pamahalaan"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Na-dismiss na ang bubble."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-tl/strings_tv.xml b/libs/WindowManager/Shell/res/values-tl/strings_tv.xml new file mode 100644 index 000000000000..b01c1115cd34 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-tl/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Picture-in-Picture"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Walang pamagat na programa)"</string> + <string name="pip_close" msgid="9135220303720555525">"Isara ang PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Full screen"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-tr/strings.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml new file mode 100644 index 000000000000..b3276dad50e7 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-tr/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Kapat"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Genişlet"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Ayarlar"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menü"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>, pencere içinde pencere özelliğini kullanıyor"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> uygulamasının bu özelliği kullanmasını istemiyorsanız dokunarak ayarları açın ve söz konusu özelliği kapatın."</string> + <string name="pip_play" msgid="3496151081459417097">"Oynat"</string> + <string name="pip_pause" msgid="690688849510295232">"Duraklat"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Sonrakine atla"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Öncekine atla"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Yeniden boyutlandır"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Uygulama bölünmüş ekranda çalışmayabilir."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Uygulama bölünmüş ekranı desteklemiyor."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Uygulama ikincil ekranda çalışmayabilir."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Uygulama ikincil ekranlarda başlatılmayı desteklemiyor."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Bölünmüş ekran ayırıcı"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Solda tam ekran"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Solda %70"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Solda %50"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Solda %30"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Sağda tam ekran"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Üstte tam ekran"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Üstte %70"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Üstte %50"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Üstte %30"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Altta tam ekran"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Tek el modunu kullanma"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Çıkmak için ekranın alt kısmından yukarı kaydırın veya uygulamanın üzerinde herhangi bir yere dokunun"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Tek el modunu başlat"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Tek el modundan çık"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> baloncukları için ayarlar"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Taşma"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Yığına geri ekle"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g> uygulamasından <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> uygulamasından <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ve diğer <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Sol üste taşı"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Sağ üste taşı"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Sol alta taşı"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Sağ alta taşı"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ayarları"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Baloncuğu kapat"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Görüşmeyi baloncuk olarak görüntüleme"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Baloncukları kullanarak sohbet edin"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Yeni görüşmeler kayan simgeler veya baloncuk olarak görünür. Açmak için baloncuğa dokunun. Baloncuğu taşımak için sürükleyin."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Baloncukları istediğiniz zaman kontrol edin"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Bu uygulamanın baloncuklarını kapatmak için Yönet\'e dokunun"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Anladım"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Son kapatılan baloncuk yok"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Son baloncuklar ve kapattığınız baloncuklar burada görünür"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Baloncuk"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Yönet"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balon kapatıldı."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-tr/strings_tv.xml b/libs/WindowManager/Shell/res/values-tr/strings_tv.xml new file mode 100644 index 000000000000..c92c4d02f465 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-tr/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Pencere İçinde Pencere"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Başlıksız program)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP\'yi kapat"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Tam ekran"</string> +</resources> 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-uk/strings.xml b/libs/WindowManager/Shell/res/values-uk/strings.xml new file mode 100644 index 000000000000..8e303cf45a39 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-uk/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Закрити"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Розгорнути"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Налаштування"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Меню"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"У додатку <xliff:g id="NAME">%s</xliff:g> є функція \"Картинка в картинці\""</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Щоб додаток <xliff:g id="NAME">%s</xliff:g> не використовував цю функцію, вимкніть її в налаштуваннях."</string> + <string name="pip_play" msgid="3496151081459417097">"Відтворити"</string> + <string name="pip_pause" msgid="690688849510295232">"Призупинити"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Перейти далі"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Перейти назад"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Змінити розмір"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Додаток може не працювати в режимі розділеного екрана."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Додаток не підтримує розділення екрана."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Додаток може не працювати на додатковому екрані."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Додаток не підтримує запуск на додаткових екранах."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Розділювач екрана"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ліве вікно на весь екран"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Ліве вікно на 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Ліве вікно на 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Ліве вікно на 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Праве вікно на весь екран"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Верхнє вікно на весь екран"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Верхнє вікно на 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Верхнє вікно на 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Верхнє вікно на 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Нижнє вікно на весь екран"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Як користуватися режимом керування однією рукою"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Щоб вийти, проведіть пальцем по екрану знизу вгору або торкніться екрана над додатком"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Увімкнути режим керування однією рукою"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Вийти з режиму керування однією рукою"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Налаштування спливаючих чатів від додатка <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Додаткове меню"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Додати в список"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"Cповіщення \"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>\" від додатка <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"Сповіщення \"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>\" від додатка <xliff:g id="APP_NAME">%2$s</xliff:g> (і ще <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>)"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Перемістити ліворуч угору"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Перемістити праворуч угору"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Перемістити ліворуч униз"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Перемістити праворуч униз"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Налаштування параметра \"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>\""</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Закрити підказку"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Не показувати спливаючі чати для розмов"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Спливаючий чат"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Нові повідомлення чату з\'являються у вигляді спливаючих значків. Щоб відкрити чат, натисніть його, а щоб перемістити – перетягніть."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Контроль спливаючих чатів"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Натисніть \"Налаштувати\", щоб вимкнути спливаючі чати від цього додатка"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Зрозуміло"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Немає нещодавніх спливаючих чатів"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Тут з\'являтимуться нещодавні й закриті спливаючі чати"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Спливаюче сповіщення"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Налаштувати"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Спливаюче сповіщення закрито."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-uk/strings_tv.xml b/libs/WindowManager/Shell/res/values-uk/strings_tv.xml new file mode 100644 index 000000000000..74d4723d7850 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-uk/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Картинка в картинці"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Програма без назви)"</string> + <string name="pip_close" msgid="9135220303720555525">"Закрити PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"На весь екран"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ur/strings.xml b/libs/WindowManager/Shell/res/values-ur/strings.xml new file mode 100644 index 000000000000..4b0adc640ddd --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ur/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"بند کریں"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"پھیلائیں"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"ترتیبات"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"مینو"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> تصویر میں تصویر میں ہے"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"اگر آپ نہیں چاہتے ہیں کہ <xliff:g id="NAME">%s</xliff:g> اس خصوصیت کا استعمال کرے تو ترتیبات کھولنے کے لیے تھپتھپا کر اسے آف کرے۔"</string> + <string name="pip_play" msgid="3496151081459417097">"چلائیں"</string> + <string name="pip_pause" msgid="690688849510295232">"موقوف کریں"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"نظرانداز کرکے اگلے پر جائیں"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"نظرانداز کرکے پچھلے پر جائیں"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"سائز تبدیل کریں"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"ممکن ہے کہ ایپ اسپلٹ اسکرین کے ساتھ کام نہ کرے۔"</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ایپ سپلٹ اسکرین کو سپورٹ نہیں کرتی۔"</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ممکن ہے ایپ ثانوی ڈسپلے پر کام نہ کرے۔"</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ایپ ثانوی ڈسپلیز پر شروعات کا تعاون نہیں کرتی۔"</string> + <string name="accessibility_divider" msgid="703810061635792791">"سپلٹ اسکرین تقسیم کار"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"بائیں فل اسکرین"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"بائیں %70"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"بائیں %50"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"بائیں %30"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"دائیں فل اسکرین"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"بالائی فل اسکرین"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"اوپر %70"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"اوپر %50"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"اوپر %30"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"نچلی فل اسکرین"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"ایک ہاتھ کی وضع کا استعمال کرنا"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"باہر نکلنے کیلئے، اسکرین کے نیچے سے اوپر کی طرف سوائپ کریں یا ایپ کے اوپر کہیں بھی تھپتھپائیں"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"ایک ہاتھ کی وضع شروع کریں"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"ایک ہاتھ کی وضع سے باہر نکلیں"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> بلبلوں کے لیے ترتیبات"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"اوورفلو"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"انبار میں واپس شامل کریں"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g> کی جانب سے <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> اور <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> مزید سے <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"اوپر بائیں جانب لے جائیں"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"اوپر دائیں جانب لے جائيں"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"نیچے بائیں جانب لے جائیں"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"نیچے دائیں جانب لے جائیں"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ترتیبات"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"بلبلہ برخاست کریں"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"گفتگو بلبلہ نہ کریں"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"بلبلے کے ذریعے چیٹ کریں"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"نئی گفتگوئیں فلوٹنگ آئیکن یا بلبلے کے طور پر ظاہر ہوں گی۔ بلبلہ کھولنے کے لیے تھپتھپائیں۔ اسے منتقل کرنے کے لیے گھسیٹیں۔"</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"کسی بھی وقت بلبلے کو کنٹرول کریں"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"اس ایپ سے بلبلوں کو آف کرنے کے لیے نظم کریں پر تھپتھپائیں"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"سمجھ آ گئی"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"کوئی حالیہ بلبلہ نہیں"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"حالیہ بلبلے اور برخاست شدہ بلبلے یہاں ظاہر ہوں گے"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"بلبلہ"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"نظم کریں"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"بلبلہ برخاست کر دیا گیا۔"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-ur/strings_tv.xml b/libs/WindowManager/Shell/res/values-ur/strings_tv.xml new file mode 100644 index 000000000000..317953309947 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-ur/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"تصویر میں تصویر"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(بلا عنوان پروگرام)"</string> + <string name="pip_close" msgid="9135220303720555525">"PIP بند کریں"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"فُل اسکرین"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-uz/strings.xml b/libs/WindowManager/Shell/res/values-uz/strings.xml new file mode 100644 index 000000000000..74b135d44522 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-uz/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Yopish"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Yoyish"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Sozlamalar"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menyu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> tasvir ustida tasvir rejimida"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ilovasi uchun bu funksiyani sozlamalar orqali faolsizlantirish mumkin."</string> + <string name="pip_play" msgid="3496151081459417097">"Ijro"</string> + <string name="pip_pause" msgid="690688849510295232">"Pauza"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Keyingisiga o‘tish"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Avvalgisiga qaytish"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Oʻlchamini oʻzgartirish"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Bu ilova ekranni ikkiga ajratish rejimini dastaklamaydi."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Bu ilova ekranni bo‘lish xususiyatini qo‘llab-quvvatlamaydi."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Bu ilova qo‘shimcha ekranda ishlamasligi mumkin."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Bu ilova qo‘shimcha ekranlarda ishga tushmaydi."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Ekranni ikkiga bo‘lish chizig‘i"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Chapda to‘liq ekran"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Chapda 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Chapda 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Chapda 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"O‘ngda to‘liq ekran"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Tepada to‘liq ekran"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Tepada 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Tepada 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Tepada 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Pastda to‘liq ekran"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Ixcham rejimdan foydalanish"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Chiqish uchun ekran pastidan tepaga suring yoki ilovaning tepasidagi istalgan joyga bosing."</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Ixcham rejimni ishga tushirish"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Ixcham rejimdan chiqish"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> bulutchalari uchun sozlamalar"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Kengaytirilgan"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Yana toʻplamga kiritish"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>, <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g> ilovasidan <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> va yana <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> ta bildirishnoma"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Yuqori chapga surish"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Yuqori oʻngga surish"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Quyi chapga surish"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Quyi oʻngga surish"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> sozlamalari"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Bulutchani yopish"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Suhbatlar bulutchalar shaklida chiqmasin"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Bulutchalar yordamida subhatlashish"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Yangi xabarlar qalqib chiquvchi belgilar yoki bulutchalar kabi chiqadi. Xabarni ochish uchun bildirishnoma ustiga bosing. Xabarni qayta joylash uchun bildirishnomani suring."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Bulutchalardagi bildirishnomalar"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Bu ilova bulutchalarini faolsizlantirish uchun Boshqarish tugmasini bosing"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"OK"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Hech qanday bulutcha topilmadi"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Eng oxirgi va yopilgan bulutchali chatlar shu yerda chiqadi"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Pufaklar"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Boshqarish"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bulutcha yopildi."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-uz/strings_tv.xml b/libs/WindowManager/Shell/res/values-uz/strings_tv.xml new file mode 100644 index 000000000000..ae5a647301c8 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-uz/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Tasvir ustida tasvir"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Nomsiz)"</string> + <string name="pip_close" msgid="9135220303720555525">"Kadr ichida kadr – chiqish"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Butun ekran"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-vi/strings.xml b/libs/WindowManager/Shell/res/values-vi/strings.xml new file mode 100644 index 000000000000..ce372317b0b8 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-vi/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Đóng"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Mở rộng"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Cài đặt"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> đang ở chế độ ảnh trong ảnh"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Nếu bạn không muốn <xliff:g id="NAME">%s</xliff:g> sử dụng tính năng này, hãy nhấn để mở cài đặt và tắt tính năng này."</string> + <string name="pip_play" msgid="3496151081459417097">"Phát"</string> + <string name="pip_pause" msgid="690688849510295232">"Tạm dừng"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Chuyển tới mục tiếp theo"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Chuyển về mục trước"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Đổi kích thước"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Ứng dụng có thể không hoạt động với tính năng chia đôi màn hình."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Ứng dụng không hỗ trợ chia đôi màn hình."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Ứng dụng có thể không hoạt động trên màn hình phụ."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Ứng dụng không hỗ trợ khởi chạy trên màn hình phụ."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Bộ chia chia đôi màn hình"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Toàn màn hình bên trái"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Trái 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Trái 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Trái 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Toàn màn hình bên phải"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Toàn màn hình phía trên"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Trên 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Trên 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Trên 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Toàn màn hình phía dưới"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Cách dùng chế độ một tay"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Để thoát, hãy vuốt lên từ cuối màn hình hoặc nhấn vào vị trí bất kỳ phía trên ứng dụng"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Bắt đầu chế độ một tay"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Thoát khỏi chế độ một tay"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Tùy chọn cài đặt cho bong bóng trò chuyện <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Trình đơn mục bổ sung"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Thêm lại vào ngăn xếp"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> của <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> từ <xliff:g id="APP_NAME">%2$s</xliff:g> và <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> bong bóng khác"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Chuyển lên trên cùng bên trái"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Chuyển lên trên cùng bên phải"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Chuyển tới dưới cùng bên trái"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Chuyển tới dưới cùng bên phải"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"Cài đặt <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Đóng bong bóng"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Dừng sử dụng bong bóng cho cuộc trò chuyện"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Trò chuyện bằng bong bóng trò chuyện"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Các cuộc trò chuyện mới sẽ xuất hiện dưới dạng biểu tượng nổi hoặc bong bóng trò chuyện. Nhấn để mở bong bóng trò chuyện. Kéo để di chuyển bong bóng trò chuyện."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Kiểm soát bong bóng bất cứ lúc nào"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Nhấn vào nút Quản lý để tắt bong bóng trò chuyện từ ứng dụng này"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Đã hiểu"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Không có bong bóng trò chuyện nào gần đây"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Bong bóng trò chuyện đã đóng và bong bóng trò chuyện gần đây sẽ xuất hiện ở đây"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Bong bóng"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Quản lý"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Đã đóng bong bóng."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-vi/strings_tv.xml b/libs/WindowManager/Shell/res/values-vi/strings_tv.xml new file mode 100644 index 000000000000..082d12596076 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-vi/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Hình trong hình"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Không có chương trình tiêu đề)"</string> + <string name="pip_close" msgid="9135220303720555525">"Đóng PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Toàn màn hình"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml new file mode 100644 index 000000000000..3143130fa4ce --- /dev/null +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"关闭"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"展开"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"设置"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"菜单"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>目前位于“画中画”中"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"如果您不想让“<xliff:g id="NAME">%s</xliff:g>”使用此功能,请点按以打开设置,然后关闭此功能。"</string> + <string name="pip_play" msgid="3496151081459417097">"播放"</string> + <string name="pip_pause" msgid="690688849510295232">"暂停"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"跳到下一个"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"跳到上一个"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"调整大小"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"应用可能无法在分屏模式下正常运行。"</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"应用不支持分屏。"</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"应用可能无法在辅显示屏上正常运行。"</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"应用不支持在辅显示屏上启动。"</string> + <string name="accessibility_divider" msgid="703810061635792791">"分屏分隔线"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"左侧全屏"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"左侧 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"左侧 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"左侧 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"右侧全屏"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"顶部全屏"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"顶部 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"顶部 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"顶部 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"底部全屏"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"使用单手模式"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"如需退出,请从屏幕底部向上滑动,或点按应用上方的任意位置"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"启动单手模式"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"退出单手模式"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g>对话泡的设置"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"菜单"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"重新加入叠放"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g>:<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="APP_NAME">%2$s</xliff:g>和另外 <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> 个应用:<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"移至左上角"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"移至右上角"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"移至左下角"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"移至右下角"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>设置"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"关闭对话泡"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"不以对话泡形式显示对话"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"使用对话泡聊天"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"新对话会以浮动图标或对话泡形式显示。点按即可打开对话泡。拖动即可移动对话泡。"</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"随时控制对话泡"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"点按“管理”按钮,可关闭来自此应用的对话泡"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"知道了"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"最近没有对话泡"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"此处会显示最近的对话泡和已关闭的对话泡"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"气泡"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"已关闭对话泡。"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml new file mode 100644 index 000000000000..cb3fcf7c4c16 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"画中画"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(节目没有标题)"</string> + <string name="pip_close" msgid="9135220303720555525">"关闭画中画"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"全屏"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml new file mode 100644 index 000000000000..4f8bfe016f6f --- /dev/null +++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"關閉"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"展開"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"設定"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"選單"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"「<xliff:g id="NAME">%s</xliff:g>」目前在畫中畫模式"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"如果您不想「<xliff:g id="NAME">%s</xliff:g>」使用此功能,請輕按以開啟設定,然後停用此功能。"</string> + <string name="pip_play" msgid="3496151081459417097">"播放"</string> + <string name="pip_pause" msgid="690688849510295232">"暫停"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"跳到下一個"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"跳到上一個"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"調整大小"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"應用程式可能無法在分割畫面中運作。"</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"應用程式不支援分割畫面。"</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"應用程式可能無法在次要顯示屏上運作。"</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"應用程式無法在次要顯示屏上啟動。"</string> + <string name="accessibility_divider" msgid="703810061635792791">"分割畫面分隔線"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"左邊全螢幕"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"左邊 70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"左邊 50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"左邊 30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"右邊全螢幕"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"頂部全螢幕"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"頂部 70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"頂部 50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"頂部 30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"底部全螢幕"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"使用單手模式"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"如要退出,請從螢幕底部向上滑動,或輕按應用程式上方的任何位置"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"開始單手模式"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"結束單手模式"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"「<xliff:g id="APP_NAME">%1$s</xliff:g>」小視窗設定"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"顯示更多"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"加回堆疊"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"來自「<xliff:g id="APP_NAME">%2$s</xliff:g>」的 <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"來自「<xliff:g id="APP_NAME">%2$s</xliff:g>」及另外 <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> 個應用程式的<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"移去左上角"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"移去右上角"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"移去左下角"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"移去右下角"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"「<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>」設定"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"關閉小視窗氣泡"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"不要透過小視窗顯示對話"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"使用小視窗進行即時通訊"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"新對話會以浮動圖示 (小視窗) 顯示。輕按即可開啟小視窗。拖曳即可移動小視窗。"</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"隨時控制小視窗設定"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"輕按「管理」即可關閉此應用程式的小視窗"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"知道了"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"沒有最近曾使用的小視窗"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"最近使用和關閉的小視窗會在這裡顯示"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"氣泡"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"對話氣泡已關閉。"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml new file mode 100644 index 000000000000..956243ed6e6d --- /dev/null +++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"畫中畫"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(沒有標題的節目)"</string> + <string name="pip_close" msgid="9135220303720555525">"關閉 PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"全螢幕"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml new file mode 100644 index 000000000000..6fb8ed963ba7 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"關閉"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"展開"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"設定"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"選單"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"「<xliff:g id="NAME">%s</xliff:g>」目前在子母畫面中"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"如果你不想讓「<xliff:g id="NAME">%s</xliff:g>」使用這項功能,請輕觸開啟設定頁面,然後停用此功能。"</string> + <string name="pip_play" msgid="3496151081459417097">"播放"</string> + <string name="pip_pause" msgid="690688849510295232">"暫停"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"跳到下一個"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"跳到上一個"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"調整大小"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"應用程式可能無法在分割畫面中運作。"</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"這個應用程式不支援分割畫面。"</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"應用程式可能無法在次要顯示器上運作。"</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"應用程式無法在次要顯示器上啟動。"</string> + <string name="accessibility_divider" msgid="703810061635792791">"分割畫面分隔線"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"以全螢幕顯示左側畫面"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"以 70% 的螢幕空間顯示左側畫面"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"以 50% 的螢幕空間顯示左側畫面"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"以 30% 的螢幕空間顯示左側畫面"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"以全螢幕顯示右側畫面"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"以全螢幕顯示頂端畫面"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"以 70% 的螢幕空間顯示頂端畫面"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"以 50% 的螢幕空間顯示頂端畫面"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"以 30% 的螢幕空間顯示頂端畫面"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"以全螢幕顯示底部畫面"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"使用單手模式"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"如要退出,請從螢幕底部向上滑動,或輕觸應用程式上的任何位置"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"啟動單手模式"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"結束單手模式"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"「<xliff:g id="APP_NAME">%1$s</xliff:g>」對話框的設定"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"溢位"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"重新加入堆疊"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="APP_NAME">%2$s</xliff:g>:<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"「<xliff:g id="APP_NAME">%2$s</xliff:g>」和其他 <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> 個應用程式:<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"移至左上方"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"移至右上方"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"移至左下方"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"移至右下方"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"「<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>」設定"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"關閉對話框"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"不要以對話框形式顯示對話"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"透過對話框來聊天"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"新的對話會以浮動圖示或對話框形式顯示。輕觸即可開啟對話框,拖曳則可移動對話框。"</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"你隨時可以控管對話框的各項設定"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"輕觸 [管理] 即可關閉來自這個應用程式的對話框"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"我知道了"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"最近沒有任何對話框"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"最近的對話框和已關閉的對話框會顯示在這裡"</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"泡泡"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"已關閉泡泡。"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml new file mode 100644 index 000000000000..08b2f4bbca89 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"子母畫面"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(無標題的節目)"</string> + <string name="pip_close" msgid="9135220303720555525">"關閉子母畫面"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"全螢幕"</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-zu/strings.xml b/libs/WindowManager/Shell/res/values-zu/strings.xml new file mode 100644 index 000000000000..cab277647d26 --- /dev/null +++ b/libs/WindowManager/Shell/res/values-zu/strings.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 xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="pip_phone_close" msgid="5783752637260411309">"Vala"</string> + <string name="pip_phone_expand" msgid="2579292903468287504">"Nweba"</string> + <string name="pip_phone_settings" msgid="5468987116750491918">"Izilungiselelo"</string> + <string name="pip_menu_title" msgid="5393619322111827096">"Imenyu"</string> + <string name="pip_notification_title" msgid="1347104727641353453">"U-<xliff:g id="NAME">%s</xliff:g> ungaphakathi kwesithombe esiphakathi kwesithombe"</string> + <string name="pip_notification_message" msgid="8854051911700302620">"Uma ungafuni i-<xliff:g id="NAME">%s</xliff:g> ukuthi isebenzise lesi sici, thepha ukuze uvule izilungiselelo uphinde uyivale."</string> + <string name="pip_play" msgid="3496151081459417097">"Dlala"</string> + <string name="pip_pause" msgid="690688849510295232">"Misa isikhashana"</string> + <string name="pip_skip_to_next" msgid="8403429188794867653">"Yeqela kokulandelayo"</string> + <string name="pip_skip_to_prev" msgid="7172158111196394092">"Yeqela kokwangaphambilini"</string> + <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Shintsha usayizi"</string> + <string name="dock_forced_resizable" msgid="1749750436092293116">"Izinhlelo zokusebenza kungenzeka zingasebenzi ngesikrini esihlukanisiwe."</string> + <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Uhlelo lokusebenza alusekeli isikrini esihlukanisiwe."</string> + <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Uhlelo lokusebenza kungenzeka lungasebenzi kusibonisi sesibili."</string> + <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Uhlelo lokusebenza alusekeli ukuqalisa kuzibonisi zesibili."</string> + <string name="accessibility_divider" msgid="703810061635792791">"Isihlukanisi sokuhlukanisa isikrini"</string> + <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Isikrini esigcwele esingakwesokunxele"</string> + <string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Kwesokunxele ngo-70%"</string> + <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Kwesokunxele ngo-50%"</string> + <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Kwesokunxele ngo-30%"</string> + <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Isikrini esigcwele esingakwesokudla"</string> + <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Isikrini esigcwele esiphezulu"</string> + <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Okuphezulu okungu-70%"</string> + <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Okuphezulu okungu-50%"</string> + <string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"Okuphezulu okungu-30%"</string> + <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Ngaphansi kwesikrini esigcwele"</string> + <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Ukusebenzisa imodi yesandla esisodwa"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Ukuze uphume, swayipha ngaphezulu kusuka ngezansi kwesikrini noma thepha noma kuphi ngenhla kohlelo lokusebenza"</string> + <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Qalisa imodi yesandla esisodwa"</string> + <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Phuma kumodi yesandla esisodwa"</string> + <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Izilungiselelo zamabhamuza e-<xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Ukuphuphuma"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Engeza emuva kusitaki"</string> + <string name="bubble_content_description_single" msgid="8495748092720065813">"I-<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> kusuka ku-<xliff:g id="APP_NAME">%2$s</xliff:g>"</string> + <string name="bubble_content_description_stack" msgid="8071515017164630429">"I-<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> kusukela ku-<xliff:g id="APP_NAME">%2$s</xliff:g> nokungu-<xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> ngaphezulu"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Hambisa phezulu kwesokunxele"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Hambisa phezulu ngakwesokudla"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Hambisa inkinobho ngakwesokunxele"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Hambisa inkinobho ngakwesokudla"</string> + <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> izilungiselelo"</string> + <string name="bubble_dismiss_text" msgid="8816558050659478158">"Cashisa ibhamuza"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ungayibhamuzi ingxoxo"</string> + <string name="bubbles_user_education_title" msgid="2112319053732691899">"Xoxa usebenzisa amabhamuza"</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"Izingxoxo ezintsha zivela njengezithonjana ezintantayo, noma amabhamuza. Thepha ukuze uvule ibhamuza. Hudula ukuze ulihambise."</string> + <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Lawula amabhamuza noma nini"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Thepha okuthi Phatha ukuvala amabhamuza kusuka kulolu hlelo lokusebenza"</string> + <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Ngiyezwa"</string> + <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Awekho amabhamuza akamuva"</string> + <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Amabhamuza akamuva namabhamuza asusiwe azobonakala lapha."</string> + <string name="notification_bubble_title" msgid="6082910224488253378">"Ibhamuza"</string> + <string name="manage_bubbles_text" msgid="7730624269650594419">"Phatha"</string> + <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Ibhamuza licashisiwe."</string> +</resources> diff --git a/libs/WindowManager/Shell/res/values-zu/strings_tv.xml b/libs/WindowManager/Shell/res/values-zu/strings_tv.xml new file mode 100644 index 000000000000..89c7f498652d --- /dev/null +++ b/libs/WindowManager/Shell/res/values-zu/strings_tv.xml @@ -0,0 +1,24 @@ +<?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" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Isithombe-esithombeni"</string> + <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Alukho uhlelo lwesihloko)"</string> + <string name="pip_close" msgid="9135220303720555525">"Vala i-PIP"</string> + <string name="pip_fullscreen" msgid="7278047353591302554">"Iskrini esigcwele"</string> +</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..350beafae961 --- /dev/null +++ b/libs/WindowManager/Shell/res/values/colors.xml @@ -0,0 +1,43 @@ +<?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> + + <!-- Background for the various drop targets when handling drag and drop. --> + <color name="drop_outline_background">#330000FF</color> + + <!-- Bubbles --> + <color name="bubbles_light">#FFFFFF</color> + <color name="bubbles_dark">@color/GM2_grey_800</color> + <color name="bubbles_icon_tint">@color/GM2_grey_700</color> + + <!-- GM2 colors --> + <color name="GM2_grey_200">#E8EAED</color> + <color name="GM2_grey_700">#5F6368</color> + <color name="GM2_grey_800">#3C4043</color> + + <!-- Splash screen --> + <color name="splash_screen_bg_light">#FFFFFF</color> + <color name="splash_screen_bg_dark">#000000</color> + <color name="splash_window_background_default">@color/splash_screen_bg_light</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..13f1fddfdfb6 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -1,21 +1,51 @@ <?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> + + <!-- Animation duration for translating of one handed when trigger / dismiss. --> + <integer name="config_one_handed_translate_animation_duration">300</integer> + + <!-- One handed mode default offset % of display size --> + <fraction name="config_one_handed_offset">40%</fraction> + + <!-- 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> + + <!-- one handed background panel default color RGB --> + <item name="config_one_handed_background_rgb" format="float" type="dimen">0.5</item> + + <!-- one handed background panel default alpha --> + <item name="config_one_handed_background_alpha" format="float" type="dimen">0.5</item> +</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..034e65c608a3 --- /dev/null +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -0,0 +1,176 @@ +<?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 width and height of the PiP expand action. --> + <dimen name="pip_expand_action_size">60dp</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> + + <!-- PIP stash offset size, which is the width of visible PIP region when stashed. --> + <dimen name="pip_stash_offset">32dp</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> + + <!-- The amount to inset the drop target regions from the edge of the display --> + <dimen name="drop_layout_display_margin">16dp</dimen> + + <!-- The menu grid size for bubble menu. --> + <dimen name="bubble_grid_item_icon_width">20dp</dimen> + <dimen name="bubble_grid_item_icon_height">20dp</dimen> + <dimen name="bubble_grid_item_icon_top_margin">12dp</dimen> + <dimen name="bubble_grid_item_icon_bottom_margin">4dp</dimen> + <dimen name="bubble_grid_item_icon_side_margin">22dp</dimen> + + <!-- How much each bubble is elevated. --> + <dimen name="bubble_elevation">1dp</dimen> + <!-- How much the bubble flyout text container is elevated. --> + <dimen name="bubble_flyout_elevation">4dp</dimen> + <!-- How much padding is around the left and right sides of the flyout text. --> + <dimen name="bubble_flyout_padding_x">12dp</dimen> + <!-- How much padding is around the top and bottom of the flyout text. --> + <dimen name="bubble_flyout_padding_y">10dp</dimen> + <!-- Size of the triangle that points from the flyout to the bubble stack. --> + <dimen name="bubble_flyout_pointer_size">6dp</dimen> + <!-- How much space to leave between the flyout (tip of the arrow) and the bubble stack. --> + <dimen name="bubble_flyout_space_from_bubble">8dp</dimen> + <!-- How much space to leave between the flyout text and the avatar displayed in the flyout. --> + <dimen name="bubble_flyout_avatar_message_space">6dp</dimen> + <!-- Padding between status bar and bubbles when displayed in expanded state --> + <dimen name="bubble_padding_top">16dp</dimen> + <!-- Max amount of space between bubbles when expanded. --> + <dimen name="bubble_max_spacing">16dp</dimen> + <!-- Size of individual bubbles. --> + <dimen name="individual_bubble_size">60dp</dimen> + <!-- Size of bubble bitmap. --> + <dimen name="bubble_bitmap_size">52dp</dimen> + <!-- Extra padding added to the touchable rect for bubbles so they are easier to grab. --> + <dimen name="bubble_touch_padding">12dp</dimen> + <!-- Size of the circle around the bubbles when they're in the dismiss target. --> + <dimen name="bubble_dismiss_encircle_size">52dp</dimen> + <!-- Padding around the view displayed when the bubble is expanded --> + <dimen name="bubble_expanded_view_padding">4dp</dimen> + <!-- This should be at least the size of bubble_expanded_view_padding; it is used to include + a slight touch slop around the expanded view. --> + <dimen name="bubble_expanded_view_slop">8dp</dimen> + <!-- Default (and minimum) height of the expanded view shown when the bubble is expanded --> + <dimen name="bubble_expanded_default_height">180dp</dimen> + <!-- Default height of bubble overflow --> + <dimen name="bubble_overflow_height">480dp</dimen> + <!-- Bubble overflow padding when there are no bubbles --> + <dimen name="bubble_overflow_empty_state_padding">16dp</dimen> + <!-- Padding of container for overflow bubbles --> + <dimen name="bubble_overflow_padding">15dp</dimen> + <!-- Padding of label for bubble overflow view --> + <dimen name="bubble_overflow_text_padding">7dp</dimen> + <!-- Height of bubble overflow empty state illustration --> + <dimen name="bubble_empty_overflow_image_height">200dp</dimen> + <!-- Padding of bubble overflow empty state subtitle --> + <dimen name="bubble_empty_overflow_subtitle_padding">50dp</dimen> + <!-- Height of the triangle that points to the expanded bubble --> + <dimen name="bubble_pointer_height">8dp</dimen> + <!-- Width of the triangle that points to the expanded bubble --> + <dimen name="bubble_pointer_width">12dp</dimen> + <!-- Extra padding around the dismiss target for bubbles --> + <dimen name="bubble_dismiss_slop">16dp</dimen> + <!-- Height of button allowing users to adjust settings for bubbles. --> + <dimen name="bubble_manage_button_height">48dp</dimen> + <!-- Height of an item in the bubble manage menu. --> + <dimen name="bubble_menu_item_height">60dp</dimen> + <!-- Max width of the message bubble--> + <dimen name="bubble_message_max_width">144dp</dimen> + <!-- Min width of the message bubble --> + <dimen name="bubble_message_min_width">32dp</dimen> + <!-- Interior padding of the message bubble --> + <dimen name="bubble_message_padding">4dp</dimen> + <!-- Offset between bubbles in their stacked position. --> + <dimen name="bubble_stack_offset">10dp</dimen> + <!-- Offset between stack y and animation y for bubble swap. --> + <dimen name="bubble_swap_animation_offset">15dp</dimen> + <!-- How far offscreen the bubble stack rests. Cuts off padding and part of icon bitmap. --> + <dimen name="bubble_stack_offscreen">9dp</dimen> + <!-- How far down the screen the stack starts. --> + <dimen name="bubble_stack_starting_offset_y">120dp</dimen> + <!-- Space between the pointer triangle and the bubble expanded view --> + <dimen name="bubble_pointer_margin">8dp</dimen> + <!-- Padding applied to the bubble dismiss target. Touches in this padding cause the bubbles to + snap to the dismiss target. --> + <dimen name="bubble_dismiss_target_padding_x">40dp</dimen> + <dimen name="bubble_dismiss_target_padding_y">20dp</dimen> + <dimen name="bubble_manage_menu_elevation">4dp</dimen> + + <!-- Bubbles user education views --> + <dimen name="bubbles_manage_education_width">160dp</dimen> + <!-- The inset from the top bound of the manage button to place the user education. --> + <dimen name="bubbles_manage_education_top_inset">65dp</dimen> + <!-- Size of padding for the user education cling, this should at minimum be larger than + individual_bubble_size + some padding. --> + <dimen name="bubble_stack_user_education_side_inset">72dp</dimen> + + <!-- The width/height of the icon view on staring surface. --> + <dimen name="starting_surface_icon_size">108dp</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..434a000010c4 --- /dev/null +++ b/libs/WindowManager/Shell/res/values/ids.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. +--> +<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" /> + + <!-- For saving PhysicsAnimationLayout animations/animators as view tags. --> + <item type="id" name="translation_x_dynamicanimation_tag"/> + <item type="id" name="translation_y_dynamicanimation_tag"/> + <item type="id" name="translation_z_dynamicanimation_tag"/> + <item type="id" name="alpha_dynamicanimation_tag"/> + <item type="id" name="scale_x_dynamicanimation_tag"/> + <item type="id" name="scale_y_dynamicanimation_tag"/> + <item type="id" name="physics_animator_tag"/> + <item type="id" name="target_animator_tag" /> + <item type="id" name="reorder_animator_tag"/> + + <!-- Accessibility actions for bubbles. --> + <item type="id" name="action_move_top_left"/> + <item type="id" name="action_move_top_right"/> + <item type="id" name="action_move_bottom_left"/> + <item type="id" name="action_move_bottom_right"/> +</resources> diff --git a/libs/WindowManager/Shell/res/values/integers.xml b/libs/WindowManager/Shell/res/values/integers.xml new file mode 100644 index 000000000000..583bf3341a69 --- /dev/null +++ b/libs/WindowManager/Shell/res/values/integers.xml @@ -0,0 +1,25 @@ +<!-- + ~ 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> + <!-- Maximum number of bubbles to render and animate at one time. While the animations used are + lightweight translation animations, this number can be reduced on lower end devices if any + performance issues arise. --> + <integer name="bubbles_max_rendered">5</integer> + <!-- Number of columns in bubble overflow. --> + <integer name="bubbles_overflow_columns">4</integer> + <!-- Maximum number of bubbles we allow in overflow before we dismiss the oldest one. --> + <integer name="bubbles_max_overflow">16</integer> +</resources>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml new file mode 100644 index 000000000000..b1425e4eeb28 --- /dev/null +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -0,0 +1,155 @@ +<?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> + + <!-- Text used for content description of settings button in the header of expanded bubble + view. [CHAR_LIMIT=NONE] --> + <string name="bubbles_settings_button_description">Settings for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g> bubbles</string> + <!-- Content description for button that shows bubble overflow on click [CHAR LIMIT=NONE] --> + <string name="bubble_overflow_button_content_description">Overflow</string> + <!-- Action to add overflow bubble back to stack. [CHAR LIMIT=NONE] --> + <string name="bubble_accessibility_action_add_back">Add back to stack</string> + <!-- Content description when a bubble is focused. [CHAR LIMIT=NONE] --> + <string name="bubble_content_description_single"><xliff:g id="notification_title" example="some title">%1$s</xliff:g> from <xliff:g id="app_name" example="YouTube">%2$s</xliff:g></string> + <!-- Content description when the stack of bubbles is focused. [CHAR LIMIT=NONE] --> + <string name="bubble_content_description_stack"><xliff:g id="notification_title" example="some title">%1$s</xliff:g> from <xliff:g id="app_name" example="YouTube">%2$s</xliff:g> and <xliff:g id="bubble_count" example="4">%3$d</xliff:g> more</string> + <!-- Action in accessibility menu to move the stack of bubbles to the top left of the screen. [CHAR LIMIT=30] --> + <string name="bubble_accessibility_action_move_top_left">Move top left</string> + <!-- Action in accessibility menu to move the stack of bubbles to the top right of the screen. [CHAR LIMIT=30] --> + <string name="bubble_accessibility_action_move_top_right">Move top right</string> + <!-- Action in accessibility menu to move the stack of bubbles to the bottom left of the screen. [CHAR LIMIT=30]--> + <string name="bubble_accessibility_action_move_bottom_left">Move bottom left</string> + <!-- Action in accessibility menu to move the stack of bubbles to the bottom right of the screen. [CHAR LIMIT=30]--> + <string name="bubble_accessibility_action_move_bottom_right">Move bottom right</string> + <!-- Label for the button that takes the user to the notification settings for the given app. --> + <string name="bubbles_app_settings"><xliff:g id="notification_title" example="Android Messages">%1$s</xliff:g> settings</string> + <!-- Text used for the bubble dismiss area. Bubbles dragged to, or flung towards, this area will go away. [CHAR LIMIT=30] --> + <string name="bubble_dismiss_text">Dismiss bubble</string> + <!-- Button text to stop a conversation from bubbling [CHAR LIMIT=60]--> + <string name="bubbles_dont_bubble_conversation">Don\u2019t bubble conversation</string> + <!-- Title text for the bubbles feature education cling shown when a bubble is on screen for the first time. [CHAR LIMIT=60]--> + <string name="bubbles_user_education_title">Chat using bubbles</string> + <!-- Descriptive text for the bubble feature education cling shown when a bubble is on screen for the first time. [CHAR LIMIT=NONE] --> + <string name="bubbles_user_education_description">New conversations appear as floating icons, or bubbles. Tap to open bubble. Drag to move it.</string> + <!-- Title text for the bubble "manage" button tool tip highlighting where users can go to control bubble settings. [CHAR LIMIT=60]--> + <string name="bubbles_user_education_manage_title">Control bubbles anytime</string> + <!-- Descriptive text for the bubble "manage" button tool tip highlighting where users can go to control bubble settings. [CHAR LIMIT=80]--> + <string name="bubbles_user_education_manage">Tap Manage to turn off bubbles from this app</string> + <!-- Button text for dismissing the bubble "manage" button tool tip [CHAR LIMIT=20]--> + <string name="bubbles_user_education_got_it">Got it</string> + <!-- [CHAR LIMIT=NONE] Empty overflow title --> + <string name="bubble_overflow_empty_title">No recent bubbles</string> + <!-- [CHAR LIMIT=NONE] Empty overflow subtitle --> + <string name="bubble_overflow_empty_subtitle">Recent bubbles and dismissed bubbles will appear here</string> + + <!-- [CHAR LIMIT=100] Notification Importance title --> + <string name="notification_bubble_title">Bubble</string> + + <!-- The text for the manage bubbles link. [CHAR LIMIT=NONE] --> + <string name="manage_bubbles_text">Manage</string> + + <!-- Content description to tell the user a bubble has been dismissed. --> + <string name="accessibility_bubble_dismissed">Bubble dismissed.</string> + + <!-- Description of the restart button in the hint of size compatibility mode. [CHAR LIMIT=NONE] --> + <string name="restart_button_description">Tap to restart this app and go full screen.</string> + + <!-- Generic "got it" acceptance of dialog or cling [CHAR LIMIT=NONE] --> + <string name="got_it">Got it</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..afe523af7cb0 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java @@ -0,0 +1,109 @@ +/* + * 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.graphics.Point; +import android.util.Slog; +import android.util.SparseArray; +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 com.android.wm.shell.transition.Transitions; + +import java.io.PrintWriter; + +/** + * Organizes tasks presented in {@link android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN}. + */ +public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { + private static final String TAG = "FullscreenTaskListener"; + + private final SyncTransactionQueue mSyncQueue; + + private final SparseArray<SurfaceControl> mLeashByTaskId = new SparseArray<>(); + + public FullscreenTaskListener(SyncTransactionQueue syncQueue) { + mSyncQueue = syncQueue; + } + + @Override + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + if (mLeashByTaskId.get(taskInfo.taskId) != null) { + throw new IllegalStateException("Task appeared more than once: #" + taskInfo.taskId); + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Appeared: #%d", + taskInfo.taskId); + mLeashByTaskId.put(taskInfo.taskId, leash); + if (Transitions.ENABLE_SHELL_TRANSITIONS) return; + final Point positionInParent = taskInfo.positionInParent; + mSyncQueue.runInSync(t -> { + // Reset several properties back to fullscreen (PiP, for example, leaves all these + // properties in a bad state). + t.setWindowCrop(leash, null); + t.setPosition(leash, positionInParent.x, positionInParent.y); + t.setAlpha(leash, 1f); + t.setMatrix(leash, 1, 0, 0, 1); + t.show(leash); + }); + } + + @Override + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (Transitions.ENABLE_SHELL_TRANSITIONS) return; + final SurfaceControl leash = mLeashByTaskId.get(taskInfo.taskId); + final Point positionInParent = taskInfo.positionInParent; + mSyncQueue.runInSync(t -> { + // Reset several properties back. For instance, when an Activity enters PiP with + // multiple activities in the same task, a new task will be created from that Activity + // and we want reset the leash of the original task. + t.setPosition(leash, positionInParent.x, positionInParent.y); + t.setWindowCrop(leash, null); + }); + } + + @Override + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + if (mLeashByTaskId.get(taskInfo.taskId) == null) { + Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId); + return; + } + mLeashByTaskId.remove(taskInfo.taskId); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Vanished: #%d", + taskInfo.taskId); + } + + @Override + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + final String childPrefix = innerPrefix + " "; + pw.println(prefix + this); + pw.println(innerPrefix + mLeashByTaskId.size() + " Tasks"); + } + + @Override + public String toString() { + return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_FULLSCREEN); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java new file mode 100644 index 000000000000..cb54021d7a23 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java @@ -0,0 +1,246 @@ +/* + * 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 android.annotation.UiContext; +import android.app.ResourcesManager; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.res.Configuration; +import android.hardware.display.DisplayManager; +import android.os.Binder; +import android.os.IBinder; +import android.util.SparseArray; +import android.view.Display; +import android.view.SurfaceControl; +import android.window.DisplayAreaAppearedInfo; +import android.window.DisplayAreaInfo; +import android.window.DisplayAreaOrganizer; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; + +/** Display area organizer for the root/default TaskDisplayAreas */ +public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer { + + private static final String TAG = RootTaskDisplayAreaOrganizer.class.getSimpleName(); + + // Display area info. mapped by displayIds. + private final SparseArray<DisplayAreaInfo> mDisplayAreasInfo = new SparseArray<>(); + // Display area leashes. mapped by displayIds. + private final SparseArray<SurfaceControl> mLeashes = new SparseArray<>(); + + private final SparseArray<ArrayList<RootTaskDisplayAreaListener>> mListeners = + new SparseArray<>(); + + private final SparseArray<DisplayAreaContext> mDisplayAreaContexts = new SparseArray<>(); + + private final Context mContext; + + public RootTaskDisplayAreaOrganizer(Executor executor, Context context) { + super(executor); + mContext = context; + List<DisplayAreaAppearedInfo> infos = registerOrganizer(FEATURE_DEFAULT_TASK_CONTAINER); + for (int i = infos.size() - 1; i >= 0; --i) { + onDisplayAreaAppeared(infos.get(i).getDisplayAreaInfo(), infos.get(i).getLeash()); + } + } + + public void registerListener(int displayId, RootTaskDisplayAreaListener listener) { + ArrayList<RootTaskDisplayAreaListener> listeners = mListeners.get(displayId); + if (listeners == null) { + listeners = new ArrayList<>(); + mListeners.put(displayId, listeners); + } + + listeners.add(listener); + + final DisplayAreaInfo info = mDisplayAreasInfo.get(displayId); + if (info != null) { + listener.onDisplayAreaAppeared(info); + } + } + + public void unregisterListener(RootTaskDisplayAreaListener listener) { + for (int i = mListeners.size() - 1; i >= 0; --i) { + final List<RootTaskDisplayAreaListener> listeners = mListeners.valueAt(i); + if (listeners == null) continue; + listeners.remove(listener); + } + } + + public void attachToDisplayArea(int displayId, SurfaceControl.Builder b) { + final SurfaceControl sc = mLeashes.get(displayId); + b.setParent(sc); + } + + @Override + public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo, + @NonNull SurfaceControl leash) { + if (displayAreaInfo.featureId != FEATURE_DEFAULT_TASK_CONTAINER) { + throw new IllegalArgumentException( + "Unknown feature: " + displayAreaInfo.featureId + + "displayAreaInfo:" + displayAreaInfo); + } + + final int displayId = displayAreaInfo.displayId; + if (mDisplayAreasInfo.get(displayId) != null) { + throw new IllegalArgumentException( + "Duplicate DA for displayId: " + displayId + + " displayAreaInfo:" + displayAreaInfo + + " mDisplayAreasInfo.get():" + mDisplayAreasInfo.get(displayId)); + } + + mDisplayAreasInfo.put(displayId, displayAreaInfo); + mLeashes.put(displayId, leash); + + ArrayList<RootTaskDisplayAreaListener> listeners = mListeners.get(displayId); + if (listeners != null) { + for (int i = listeners.size() - 1; i >= 0; --i) { + listeners.get(i).onDisplayAreaAppeared(displayAreaInfo); + } + } + applyConfigChangesToContext(displayId, displayAreaInfo.configuration); + } + + @Override + public void onDisplayAreaVanished(@NonNull DisplayAreaInfo displayAreaInfo) { + final int displayId = displayAreaInfo.displayId; + if (mDisplayAreasInfo.get(displayId) == null) { + throw new IllegalArgumentException( + "onDisplayAreaVanished() Unknown DA displayId: " + displayId + + " displayAreaInfo:" + displayAreaInfo + + " mDisplayAreasInfo.get():" + mDisplayAreasInfo.get(displayId)); + } + + mDisplayAreasInfo.remove(displayId); + + ArrayList<RootTaskDisplayAreaListener> listeners = mListeners.get(displayId); + if (listeners != null) { + for (int i = listeners.size() - 1; i >= 0; --i) { + listeners.get(i).onDisplayAreaVanished(displayAreaInfo); + } + } + mDisplayAreaContexts.remove(displayId); + } + + @Override + public void onDisplayAreaInfoChanged(@NonNull DisplayAreaInfo displayAreaInfo) { + final int displayId = displayAreaInfo.displayId; + if (mDisplayAreasInfo.get(displayId) == null) { + throw new IllegalArgumentException( + "onDisplayAreaInfoChanged() Unknown DA displayId: " + displayId + + " displayAreaInfo:" + displayAreaInfo + + " mDisplayAreasInfo.get():" + mDisplayAreasInfo.get(displayId)); + } + + mDisplayAreasInfo.put(displayId, displayAreaInfo); + + ArrayList<RootTaskDisplayAreaListener> listeners = mListeners.get(displayId); + if (listeners != null) { + for (int i = listeners.size() - 1; i >= 0; --i) { + listeners.get(i).onDisplayAreaInfoChanged(displayAreaInfo); + } + } + applyConfigChangesToContext(displayId, displayAreaInfo.configuration); + } + + /** + * Applies the {@link Configuration} to the {@link DisplayAreaContext} specified by + * {@code displayId}. + * + * @param displayId The ID of the {@link Display} which the {@link DisplayAreaContext} is + * associated with + * @param newConfig The propagated configuration + */ + private void applyConfigChangesToContext(int displayId, @NonNull Configuration newConfig) { + DisplayAreaContext daContext = mDisplayAreaContexts.get(displayId); + if (daContext == null) { + daContext = new DisplayAreaContext(mContext, displayId); + mDisplayAreaContexts.put(displayId, daContext); + } + daContext.updateConfigurationChanges(newConfig); + } + + /** + * Returns the UI context associated with RootTaskDisplayArea specified by {@code displayId}. + */ + @Nullable + @UiContext + public Context getContext(int displayId) { + return mDisplayAreaContexts.get(displayId); + } + + 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 + "#" + mDisplayAreasInfo.size(); + } + + /** Callbacks for when root task display areas change. */ + public interface RootTaskDisplayAreaListener { + default void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo) { + } + + default void onDisplayAreaVanished(DisplayAreaInfo displayAreaInfo) { + } + + default void onDisplayAreaInfoChanged(DisplayAreaInfo displayAreaInfo) { + } + + default void dump(@NonNull PrintWriter pw, String prefix) { + } + } + + /** + * A UI context to associate with a {@link com.android.server.wm.DisplayArea}. + * + * This context receives configuration changes through {@link DisplayAreaOrganizer} callbacks + * and the core implementation is {@link Context#createTokenContext(IBinder, Display)} to apply + * the configuration updates to the {@link android.content.res.Resources}. + */ + @UiContext + public static class DisplayAreaContext extends ContextWrapper { + private final IBinder mToken = new Binder(); + private final ResourcesManager mResourcesManager = ResourcesManager.getInstance(); + + public DisplayAreaContext(@NonNull Context context, int displayId) { + super(null); + final Display display = context.getSystemService(DisplayManager.class) + .getDisplay(displayId); + attachBaseContext(context.createTokenContext(mToken, display)); + } + + private void updateConfigurationChanges(@NonNull Configuration newConfig) { + final Configuration config = getResources().getConfiguration(); + final boolean configChanged = config.diff(newConfig) != 0; + if (configChanged) { + mResourcesManager.updateResourcesForActivity(mToken, newConfig, getDisplayId()); + } + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandler.java new file mode 100644 index 000000000000..aa82339a436a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandler.java @@ -0,0 +1,47 @@ +/* + * 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; + +import android.util.Slog; + +import com.android.wm.shell.apppairs.AppPairs; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout; +import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; +import com.android.wm.shell.onehanded.OneHanded; +import com.android.wm.shell.pip.Pip; + +import java.io.PrintWriter; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * An entry point into the shell for dumping shell internal state and running adb commands. + * + * Use with {@code adb shell dumpsys activity service SystemUIService WMShell ...}. + */ +public interface ShellCommandHandler { + /** + * Dumps the shell state. + */ + void dump(PrintWriter pw); + + /** + * Handles a shell command. + */ + boolean handleCommand(final String[] args, PrintWriter pw); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java new file mode 100644 index 000000000000..fe97e24fac41 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java @@ -0,0 +1,240 @@ +/* + * 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; + +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_BOTTOM_OR_RIGHT; + +import com.android.wm.shell.apppairs.AppPairs; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout; +import com.android.wm.shell.onehanded.OneHanded; +import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; +import com.android.wm.shell.splitscreen.SplitScreen; + +import java.io.PrintWriter; +import java.util.Optional; + +/** + * An entry point into the shell for dumping shell internal state and running adb commands. + * + * Use with {@code adb shell dumpsys activity service SystemUIService WMShell ...}. + */ +public final class ShellCommandHandlerImpl { + private static final String TAG = ShellCommandHandlerImpl.class.getSimpleName(); + + private final Optional<LegacySplitScreen> mLegacySplitScreenOptional; + private final Optional<SplitScreen> mSplitScreenOptional; + private final Optional<Pip> mPipOptional; + private final Optional<OneHanded> mOneHandedOptional; + private final Optional<HideDisplayCutout> mHideDisplayCutout; + private final ShellTaskOrganizer mShellTaskOrganizer; + private final Optional<AppPairs> mAppPairsOptional; + private final ShellExecutor mMainExecutor; + private final HandlerImpl mImpl = new HandlerImpl(); + + public static ShellCommandHandler create( + ShellTaskOrganizer shellTaskOrganizer, + Optional<LegacySplitScreen> legacySplitScreenOptional, + Optional<SplitScreen> splitScreenOptional, + Optional<Pip> pipOptional, + Optional<OneHanded> oneHandedOptional, + Optional<HideDisplayCutout> hideDisplayCutout, + Optional<AppPairs> appPairsOptional, + ShellExecutor mainExecutor) { + return new ShellCommandHandlerImpl(shellTaskOrganizer, legacySplitScreenOptional, + splitScreenOptional, pipOptional, oneHandedOptional, hideDisplayCutout, + appPairsOptional, mainExecutor).mImpl; + } + + private ShellCommandHandlerImpl( + ShellTaskOrganizer shellTaskOrganizer, + Optional<LegacySplitScreen> legacySplitScreenOptional, + Optional<SplitScreen> splitScreenOptional, + Optional<Pip> pipOptional, + Optional<OneHanded> oneHandedOptional, + Optional<HideDisplayCutout> hideDisplayCutout, + Optional<AppPairs> appPairsOptional, + ShellExecutor mainExecutor) { + mShellTaskOrganizer = shellTaskOrganizer; + mLegacySplitScreenOptional = legacySplitScreenOptional; + mSplitScreenOptional = splitScreenOptional; + mPipOptional = pipOptional; + mOneHandedOptional = oneHandedOptional; + mHideDisplayCutout = hideDisplayCutout; + mAppPairsOptional = appPairsOptional; + mMainExecutor = mainExecutor; + } + + /** Dumps WM Shell internal state. */ + private void dump(PrintWriter pw) { + mShellTaskOrganizer.dump(pw, ""); + pw.println(); + pw.println(); + mPipOptional.ifPresent(pip -> pip.dump(pw)); + mLegacySplitScreenOptional.ifPresent(splitScreen -> splitScreen.dump(pw)); + mOneHandedOptional.ifPresent(oneHanded -> oneHanded.dump(pw)); + mHideDisplayCutout.ifPresent(hideDisplayCutout -> hideDisplayCutout.dump(pw)); + pw.println(); + pw.println(); + mAppPairsOptional.ifPresent(appPairs -> appPairs.dump(pw, "")); + pw.println(); + pw.println(); + mSplitScreenOptional.ifPresent(splitScreen -> splitScreen.dump(pw, "")); + } + + + /** Returns {@code true} if command was found and executed. */ + private boolean handleCommand(final String[] args, PrintWriter pw) { + if (args.length < 2) { + // Argument at position 0 is "WMShell". + return false; + } + switch (args[1]) { + case "pair": + return runPair(args, pw); + case "unpair": + return runUnpair(args, pw); + case "moveToSideStage": + return runMoveToSideStage(args, pw); + case "removeFromSideStage": + return runRemoveFromSideStage(args, pw); + case "setSideStagePosition": + return runSetSideStagePosition(args, pw); + case "setSideStageVisibility": + return runSetSideStageVisibility(args, pw); + case "help": + return runHelp(pw); + default: + return false; + } + } + + private boolean runPair(String[] args, PrintWriter pw) { + if (args.length < 4) { + // First two arguments are "WMShell" and command name. + pw.println("Error: two task ids should be provided as arguments"); + return false; + } + final int taskId1 = new Integer(args[2]); + final int taskId2 = new Integer(args[3]); + mAppPairsOptional.ifPresent(appPairs -> appPairs.pair(taskId1, taskId2)); + return true; + } + + private boolean runUnpair(String[] args, PrintWriter pw) { + if (args.length < 3) { + // First two arguments are "WMShell" and command name. + pw.println("Error: task id should be provided as an argument"); + return false; + } + final int taskId = new Integer(args[2]); + mAppPairsOptional.ifPresent(appPairs -> appPairs.unpair(taskId)); + return true; + } + + private boolean runMoveToSideStage(String[] args, PrintWriter pw) { + if (args.length < 3) { + // First arguments are "WMShell" and command name. + pw.println("Error: task id should be provided as arguments"); + return false; + } + final int taskId = new Integer(args[2]); + final int sideStagePosition = args.length > 3 + ? new Integer(args[3]) : STAGE_POSITION_BOTTOM_OR_RIGHT; + mSplitScreenOptional.ifPresent(split -> split.moveToSideStage(taskId, sideStagePosition)); + return true; + } + + private boolean runRemoveFromSideStage(String[] args, PrintWriter pw) { + if (args.length < 3) { + // First arguments are "WMShell" and command name. + pw.println("Error: task id should be provided as arguments"); + return false; + } + final int taskId = new Integer(args[2]); + mSplitScreenOptional.ifPresent(split -> split.removeFromSideStage(taskId)); + return true; + } + + private boolean runSetSideStagePosition(String[] args, PrintWriter pw) { + if (args.length < 3) { + // First arguments are "WMShell" and command name. + pw.println("Error: side stage position should be provided as arguments"); + return false; + } + final int position = new Integer(args[2]); + mSplitScreenOptional.ifPresent(split -> split.setSideStagePosition(position)); + return true; + } + + private boolean runSetSideStageVisibility(String[] args, PrintWriter pw) { + if (args.length < 3) { + // First arguments are "WMShell" and command name. + pw.println("Error: side stage position should be provided as arguments"); + return false; + } + final Boolean visible = new Boolean(args[2]); + + mSplitScreenOptional.ifPresent(split -> split.setSideStageVisibility(visible)); + return true; + } + + private boolean runHelp(PrintWriter pw) { + pw.println("Window Manager Shell commands:"); + pw.println(" help"); + pw.println(" Print this help text."); + pw.println(" <no arguments provided>"); + pw.println(" Dump Window Manager Shell internal state"); + pw.println(" pair <taskId1> <taskId2>"); + pw.println(" unpair <taskId>"); + pw.println(" Pairs/unpairs tasks with given ids."); + pw.println(" moveToSideStage <taskId> <SideStagePosition>"); + pw.println(" Move a task with given id in split-screen mode."); + pw.println(" removeFromSideStage <taskId>"); + pw.println(" Remove a task with given id in split-screen mode."); + pw.println(" setSideStagePosition <SideStagePosition>"); + pw.println(" Sets the position of the side-stage."); + pw.println(" setSideStageVisibility <true/false>"); + pw.println(" Show/hide side-stage."); + return true; + } + + private class HandlerImpl implements ShellCommandHandler { + @Override + public void dump(PrintWriter pw) { + try { + mMainExecutor.executeBlocking(() -> ShellCommandHandlerImpl.this.dump(pw)); + } catch (InterruptedException e) { + throw new RuntimeException("Failed to dump the Shell in 2s", e); + } + } + + @Override + public boolean handleCommand(String[] args, PrintWriter pw) { + try { + boolean[] result = new boolean[1]; + mMainExecutor.executeBlocking(() -> { + result[0] = ShellCommandHandlerImpl.this.handleCommand(args, pw); + }); + return result[0]; + } catch (InterruptedException e) { + throw new RuntimeException("Failed to handle Shell command in 2s", e); + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/src/com/android/wm/shell/tests/WindowManagerShellTest.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInit.java index 376875b143a1..d7010b174744 100644 --- a/libs/WindowManager/Shell/tests/src/com/android/wm/shell/tests/WindowManagerShellTest.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInit.java @@ -14,27 +14,17 @@ * limitations under the License. */ -package com.android.wm.shell.tests; +package com.android.wm.shell; -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; +import com.android.wm.shell.common.annotations.ExternalThread; /** - * Tests for the shell. + * An entry point into the shell for initializing shell internal state. */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class WindowManagerShellTest { - - WindowManagerShell mShell; - - @Test - public void testNothing() { - // Do nothing - } +@ExternalThread +public interface ShellInit { + /** + * Initializes the shell state. + */ + void init(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java new file mode 100644 index 000000000000..0958a070c82d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java @@ -0,0 +1,121 @@ +/* + * 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; + +import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN; + +import com.android.wm.shell.apppairs.AppPairs; +import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.draganddrop.DragAndDropController; +import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; +import com.android.wm.shell.splitscreen.SplitScreen; +import com.android.wm.shell.transition.Transitions; + +import java.util.Optional; + +/** + * The entry point implementation into the shell for initializing shell internal state. + */ +public class ShellInitImpl { + private static final String TAG = ShellInitImpl.class.getSimpleName(); + + private final DisplayImeController mDisplayImeController; + private final DragAndDropController mDragAndDropController; + private final ShellTaskOrganizer mShellTaskOrganizer; + private final Optional<LegacySplitScreen> mLegacySplitScreenOptional; + private final Optional<SplitScreen> mSplitScreenOptional; + private final Optional<AppPairs> mAppPairsOptional; + private final FullscreenTaskListener mFullscreenTaskListener; + private final ShellExecutor mMainExecutor; + private final Transitions mTransitions; + + private final InitImpl mImpl = new InitImpl(); + + public static ShellInit create(DisplayImeController displayImeController, + DragAndDropController dragAndDropController, + ShellTaskOrganizer shellTaskOrganizer, + Optional<LegacySplitScreen> legacySplitScreenOptional, + Optional<SplitScreen> splitScreenOptional, + Optional<AppPairs> appPairsOptional, + FullscreenTaskListener fullscreenTaskListener, + Transitions transitions, + ShellExecutor mainExecutor) { + return new ShellInitImpl(displayImeController, + dragAndDropController, + shellTaskOrganizer, + legacySplitScreenOptional, + splitScreenOptional, + appPairsOptional, + fullscreenTaskListener, + transitions, + mainExecutor).mImpl; + } + + private ShellInitImpl(DisplayImeController displayImeController, + DragAndDropController dragAndDropController, + ShellTaskOrganizer shellTaskOrganizer, + Optional<LegacySplitScreen> legacySplitScreenOptional, + Optional<SplitScreen> splitScreenOptional, + Optional<AppPairs> appPairsOptional, + FullscreenTaskListener fullscreenTaskListener, + Transitions transitions, + ShellExecutor mainExecutor) { + mDisplayImeController = displayImeController; + mDragAndDropController = dragAndDropController; + mShellTaskOrganizer = shellTaskOrganizer; + mLegacySplitScreenOptional = legacySplitScreenOptional; + mSplitScreenOptional = splitScreenOptional; + mAppPairsOptional = appPairsOptional; + mFullscreenTaskListener = fullscreenTaskListener; + mTransitions = transitions; + mMainExecutor = mainExecutor; + } + + private void init() { + // Start listening for display changes + mDisplayImeController.startMonitorDisplays(); + + mShellTaskOrganizer.addListenerForType( + mFullscreenTaskListener, TASK_LISTENER_TYPE_FULLSCREEN); + // Register the shell organizer + mShellTaskOrganizer.registerOrganizer(); + + mAppPairsOptional.ifPresent(AppPairs::onOrganizerRegistered); + mSplitScreenOptional.ifPresent(SplitScreen::onOrganizerRegistered); + + // Bind the splitscreen impl to the drag drop controller + mDragAndDropController.initialize(mSplitScreenOptional); + + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mTransitions.register(mShellTaskOrganizer); + } + } + + @ExternalThread + private class InitImpl implements ShellInit { + @Override + public void init() { + try { + mMainExecutor.executeBlocking(() -> ShellInitImpl.this.init()); + } catch (InterruptedException e) { + throw new RuntimeException("Failed to initialize the Shell in 2s", e); + } + } + } +} 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..a570c0af698d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.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; + +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_UNDEFINED; + +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; + +import android.annotation.IntDef; +import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; +import android.os.Binder; +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.StartingWindowInfo; +import android.window.TaskAppearedInfo; +import android.window.TaskOrganizer; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +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.sizecompatui.SizeCompatUI; +import com.android.wm.shell.startingsurface.StartingSurfaceDrawer; + +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; + + @IntDef(prefix = {"TASK_LISTENER_TYPE_"}, value = { + TASK_LISTENER_TYPE_UNDEFINED, + TASK_LISTENER_TYPE_FULLSCREEN, + TASK_LISTENER_TYPE_MULTI_WINDOW, + TASK_LISTENER_TYPE_PIP, + }) + 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<>(); + + private final Object mLock = new Object(); + private final StartingSurfaceDrawer mStartingSurfaceDrawer; + + /** + * In charge of showing size compat UI. Can be {@code null} if device doesn't support size + * compat. + */ + @Nullable + private final SizeCompatUI mSizeCompatUI; + + public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context) { + this(null /* taskOrganizerController */, mainExecutor, context, null /* sizeCompatUI */); + } + + public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context, @Nullable + SizeCompatUI sizeCompatUI) { + this(null /* taskOrganizerController */, mainExecutor, context, sizeCompatUI); + } + + @VisibleForTesting + ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController, ShellExecutor mainExecutor, + Context context, @Nullable SizeCompatUI sizeCompatUI) { + super(taskOrganizerController, mainExecutor); + // TODO(b/131727939) temporarily live here, the starting surface drawer should be controlled + // by a controller, that class should be create while porting + // ActivityRecord#addStartingWindow to WMShell. + mStartingSurfaceDrawer = new StartingSurfaceDrawer(context, mainExecutor); + mSizeCompatUI = sizeCompatUI; + } + + @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; + } + } + + public void createRootTask(int displayId, int windowingMode, TaskListener listener) { + ProtoLog.v(WM_SHELL_TASK_ORG, "createRootTask() displayId=%d winMode=%d listener=%s", + displayId, windowingMode, listener.toString()); + final IBinder cookie = new Binder(); + setPendingLaunchCookieListener(cookie, listener); + super.createRootTask(displayId, windowingMode, cookie); + } + + /** + * 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 addStartingWindow(StartingWindowInfo info, IBinder appToken) { + mStartingSurfaceDrawer.addStartingWindow(info, appToken); + } + + @Override + public void removeStartingWindow(int taskId) { + mStartingSurfaceDrawer.removeStartingWindow(taskId); + } + + @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; + mTasks.put(taskId, info); + final TaskListener listener = + getTaskListener(info.getTaskInfo(), true /*removeLaunchCookieIfNeeded*/); + ProtoLog.v(WM_SHELL_TASK_ORG, "Task appeared taskId=%d listener=%s", taskId, listener); + if (listener != null) { + listener.onTaskAppeared(info.getTaskInfo(), info.getLeash()); + } + notifySizeCompatUI(info.getTaskInfo(), listener); + } + + @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); + } + if (updated || !taskInfo.equalsForSizeCompat(data.getTaskInfo())) { + // Notify the size compat UI if the listener or task info changed. + notifySizeCompatUI(taskInfo, newListener); + } + } + } + + @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); + } + // Pass null for listener to remove the size compat UI on this task if there is any. + notifySizeCompatUI(taskInfo, null /* taskListener */); + } + } + + /** Gets running task by taskId. Returns {@code null} if no such task observed. */ + @Nullable + public RunningTaskInfo getRunningTaskInfo(int taskId) { + synchronized (mLock) { + final TaskAppearedInfo info = mTasks.get(taskId); + return info != null ? info.getTaskInfo() : null; + } + } + + 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; + } + + /** + * Notifies {@link SizeCompatUI} about the size compat info changed on the give Task to update + * the UI accordingly. + * + * @param taskInfo the new Task info + * @param taskListener listener to handle the Task Surface placement. {@code null} if task is + * vanished. + */ + private void notifySizeCompatUI(RunningTaskInfo taskInfo, @Nullable TaskListener taskListener) { + if (mSizeCompatUI == null) { + return; + } + + // The task is vanished, notify to remove size compat UI on this Task if there is any. + if (taskListener == null) { + mSizeCompatUI.onSizeCompatInfoChanged(taskInfo.displayId, taskInfo.taskId, + null /* taskConfig */, null /* sizeCompatActivity*/, + null /* taskListener */); + return; + } + + mSizeCompatUI.onSizeCompatInfoChanged(taskInfo.displayId, taskInfo.taskId, + taskInfo.configuration.windowConfiguration.getBounds(), + // null if the top activity not in size compat. + taskInfo.topActivityInSizeCompat ? taskInfo.topActivityToken : null, + taskListener); + } + + 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 priority goes to the listener listening to its parent. + if (runningTaskInfo.hasParentTask()) { + listener = mTaskListeners.get(runningTaskInfo.parentTaskId); + if (listener != null) return listener; + } + + // Next we try type specific listeners. + final int taskListenerType = taskInfoToTaskListenerType(runningTaskInfo); + return mTaskListeners.get(taskListenerType); + } + + @VisibleForTesting + static @TaskListenerType int taskInfoToTaskListenerType(RunningTaskInfo runningTaskInfo) { + switch (runningTaskInfo.getWindowingMode()) { + case WINDOWING_MODE_FULLSCREEN: + return TASK_LISTENER_TYPE_FULLSCREEN; + case WINDOWING_MODE_MULTI_WINDOW: + return TASK_LISTENER_TYPE_MULTI_WINDOW; + 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_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/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java new file mode 100644 index 000000000000..bb8a97344664 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.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; + +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.LauncherApps; +import android.content.pm.ShortcutInfo; +import android.graphics.Rect; +import android.graphics.Region; +import android.os.Binder; +import android.util.CloseGuard; +import android.view.SurfaceControl; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewTreeObserver; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import java.io.PrintWriter; +import java.util.concurrent.Executor; + +/** + * View that can display a task. + */ +public class TaskView extends SurfaceView implements SurfaceHolder.Callback, + ShellTaskOrganizer.TaskListener, ViewTreeObserver.OnComputeInternalInsetsListener { + + /** Callback for listening task state. */ + public interface Listener { + /** Called when the container is ready for launching activities. */ + default void onInitialized() {} + + /** Called when the container can no longer launch activities. */ + default void onReleased() {} + + /** Called when a task is created inside the container. */ + default void onTaskCreated(int taskId, ComponentName name) {} + + /** Called when a task visibility changes. */ + default void onTaskVisibilityChanged(int taskId, boolean visible) {} + + /** Called when a task is about to be removed from the stack inside the container. */ + default void onTaskRemovalStarted(int taskId) {} + + /** Called when a task is created inside the container. */ + default void onBackPressedOnTaskRoot(int taskId) {} + } + + private final CloseGuard mGuard = new CloseGuard(); + + private final ShellTaskOrganizer mTaskOrganizer; + private final Executor mShellExecutor; + + private ActivityManager.RunningTaskInfo mTaskInfo; + private WindowContainerToken mTaskToken; + private SurfaceControl mTaskLeash; + private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); + private boolean mSurfaceCreated; + private boolean mIsInitialized; + private Listener mListener; + private Executor mListenerExecutor; + + private final Rect mTmpRect = new Rect(); + private final Rect mTmpRootRect = new Rect(); + private final int[] mTmpLocation = new int[2]; + + public TaskView(Context context, ShellTaskOrganizer organizer) { + super(context, null, 0, 0, true /* disableBackgroundLayer */); + + mTaskOrganizer = organizer; + mShellExecutor = organizer.getExecutor(); + setUseAlpha(); + getHolder().addCallback(this); + mGuard.open("release"); + } + + /** + * Only one listener may be set on the view, throws an exception otherwise. + */ + public void setListener(@NonNull Executor executor, Listener listener) { + if (mListener != null) { + throw new IllegalStateException( + "Trying to set a listener when one has already been set"); + } + mListener = listener; + mListenerExecutor = executor; + } + + /** + * Launch an activity represented by {@link ShortcutInfo}. + * <p>The owner of this container must be allowed to access the shortcut information, + * as defined in {@link LauncherApps#hasShortcutHostPermission()} to use this method. + * + * @param shortcut the shortcut used to launch the activity. + * @param options options for the activity. + * @param sourceBounds the rect containing the source bounds of the clicked icon to open + * this shortcut. + */ + public void startShortcutActivity(@NonNull ShortcutInfo shortcut, + @NonNull ActivityOptions options, @Nullable Rect sourceBounds) { + prepareActivityOptions(options); + LauncherApps service = mContext.getSystemService(LauncherApps.class); + try { + service.startShortcut(shortcut, sourceBounds, options.toBundle()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Launch a new activity. + * + * @param pendingIntent Intent used to launch an activity. + * @param fillInIntent Additional Intent data, see {@link Intent#fillIn Intent.fillIn()} + * @param options options for the activity. + */ + public void startActivity(@NonNull PendingIntent pendingIntent, @Nullable Intent fillInIntent, + @NonNull ActivityOptions options) { + prepareActivityOptions(options); + try { + pendingIntent.send(mContext, 0 /* code */, fillInIntent, + null /* onFinished */, null /* handler */, null /* requiredPermission */, + options.toBundle()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void prepareActivityOptions(ActivityOptions options) { + final Binder launchCookie = new Binder(); + mShellExecutor.execute(() -> { + mTaskOrganizer.setPendingLaunchCookieListener(launchCookie, this); + }); + options.setLaunchCookie(launchCookie); + options.setLaunchWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + } + + /** + * Call when view position or size has changed. Do not call when animating. + */ + public void onLocationChanged() { + if (mTaskToken == null) { + return; + } + // Update based on the screen bounds + getBoundsOnScreen(mTmpRect); + getRootView().getBoundsOnScreen(mTmpRootRect); + if (!mTmpRootRect.contains(mTmpRect)) { + mTmpRect.offsetTo(0, 0); + } + + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setBounds(mTaskToken, mTmpRect); + // TODO(b/151449487): Enable synchronization + mTaskOrganizer.applyTransaction(wct); + } + + /** + * Release this container if it is initialized. + */ + public void release() { + performRelease(); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mGuard != null) { + mGuard.warnIfOpen(); + performRelease(); + } + } finally { + super.finalize(); + } + } + + private void performRelease() { + getHolder().removeCallback(this); + mShellExecutor.execute(() -> { + mTaskOrganizer.removeListener(this); + resetTaskInfo(); + }); + mGuard.close(); + if (mListener != null && mIsInitialized) { + mListenerExecutor.execute(() -> { + mListener.onReleased(); + }); + mIsInitialized = false; + } + } + + private void resetTaskInfo() { + mTaskInfo = null; + mTaskToken = null; + mTaskLeash = null; + } + + private void updateTaskVisibility() { + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setHidden(mTaskToken, !mSurfaceCreated /* hidden */); + mTaskOrganizer.applyTransaction(wct); + // TODO(b/151449487): Only call callback once we enable synchronization + if (mListener != null) { + final int taskId = mTaskInfo.taskId; + mListenerExecutor.execute(() -> { + mListener.onTaskVisibilityChanged(taskId, mSurfaceCreated); + }); + } + } + + @Override + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl leash) { + mTaskInfo = taskInfo; + mTaskToken = taskInfo.token; + mTaskLeash = leash; + + if (mSurfaceCreated) { + // Surface is ready, so just reparent the task to this surface control + mTransaction.reparent(mTaskLeash, getSurfaceControl()) + .show(mTaskLeash) + .apply(); + } else { + // The surface has already been destroyed before the task has appeared, + // so go ahead and hide the task entirely + updateTaskVisibility(); + } + mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, true); + // TODO: Synchronize show with the resize + onLocationChanged(); + if (taskInfo.taskDescription != null) { + setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor()); + } + + if (mListener != null) { + final int taskId = taskInfo.taskId; + final ComponentName baseActivity = taskInfo.baseActivity; + mListenerExecutor.execute(() -> { + mListener.onTaskCreated(taskId, baseActivity); + }); + } + } + + @Override + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + if (mTaskToken == null || !mTaskToken.equals(taskInfo.token)) return; + + if (mListener != null) { + final int taskId = taskInfo.taskId; + mListenerExecutor.execute(() -> { + mListener.onTaskRemovalStarted(taskId); + }); + } + mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, false); + + // Unparent the task when this surface is destroyed + mTransaction.reparent(mTaskLeash, null).apply(); + resetTaskInfo(); + } + + @Override + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (taskInfo.taskDescription != null) { + setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor()); + } + } + + @Override + public void onBackPressedOnTaskRoot(ActivityManager.RunningTaskInfo taskInfo) { + if (mTaskToken == null || !mTaskToken.equals(taskInfo.token)) return; + if (mListener != null) { + final int taskId = taskInfo.taskId; + mListenerExecutor.execute(() -> { + mListener.onBackPressedOnTaskRoot(taskId); + }); + } + } + + @Override + public void dump(@androidx.annotation.NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + final String childPrefix = innerPrefix + " "; + pw.println(prefix + this); + } + + @Override + public String toString() { + return "TaskView" + ":" + (mTaskInfo != null ? mTaskInfo.taskId : "null"); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + mSurfaceCreated = true; + if (mListener != null && !mIsInitialized) { + mIsInitialized = true; + mListenerExecutor.execute(() -> { + mListener.onInitialized(); + }); + } + mShellExecutor.execute(() -> { + if (mTaskToken == null) { + // Nothing to update, task is not yet available + return; + } + // Reparent the task when this surface is created + mTransaction.reparent(mTaskLeash, getSurfaceControl()) + .show(mTaskLeash) + .apply(); + updateTaskVisibility(); + }); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + if (mTaskToken == null) { + return; + } + onLocationChanged(); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + mSurfaceCreated = false; + mShellExecutor.execute(() -> { + if (mTaskToken == null) { + // Nothing to update, task is not yet available + return; + } + + // Unparent the task when this surface is destroyed + mTransaction.reparent(mTaskLeash, null).apply(); + updateTaskVisibility(); + }); + } + + @Override + public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { + // TODO(b/176854108): Consider to move the logic into gatherTransparentRegions since this + // is dependent on the order of listener. + // If there are multiple TaskViews, we'll set the touchable area as the root-view, then + // subtract each TaskView from it. + if (inoutInfo.touchableRegion.isEmpty()) { + inoutInfo.setTouchableInsets( + ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + View root = getRootView(); + root.getLocationInWindow(mTmpLocation); + mTmpRootRect.set(mTmpLocation[0], mTmpLocation[1], root.getWidth(), root.getHeight()); + inoutInfo.touchableRegion.set(mTmpRootRect); + } + getLocationInWindow(mTmpLocation); + mTmpRect.set(mTmpLocation[0], mTmpLocation[1], + mTmpLocation[0] + getWidth(), mTmpLocation[1] + getHeight()); + inoutInfo.touchableRegion.op(mTmpRect, Region.Op.DIFFERENCE); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + getViewTreeObserver().addOnComputeInternalInsetsListener(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + getViewTreeObserver().removeOnComputeInternalInsetsListener(this); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactory.java new file mode 100644 index 000000000000..a29e7a085a21 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactory.java @@ -0,0 +1,32 @@ +/* + * 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 android.annotation.UiContext; +import android.content.Context; + +import com.android.wm.shell.common.annotations.ExternalThread; + +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** Interface to create TaskView. */ +@ExternalThread +public interface TaskViewFactory { + /** Creates an {@link TaskView} */ + void create(@UiContext Context context, Executor executor, Consumer<TaskView> onCreate); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java new file mode 100644 index 000000000000..a5dd79b373bd --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.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; + +import android.annotation.UiContext; +import android.content.Context; + +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.common.annotations.ShellMainThread; + +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** Factory controller which can create {@link TaskView} */ +public class TaskViewFactoryController { + private final ShellTaskOrganizer mTaskOrganizer; + private final ShellExecutor mShellExecutor; + + public TaskViewFactoryController(ShellTaskOrganizer taskOrganizer, + ShellExecutor shellExecutor) { + mTaskOrganizer = taskOrganizer; + mShellExecutor = shellExecutor; + } + + /** Creates an {@link TaskView} */ + @ShellMainThread + public void create(@UiContext Context context, Executor executor, Consumer<TaskView> onCreate) { + TaskView taskView = new TaskView(context, mTaskOrganizer); + executor.execute(() -> { + onCreate.accept(taskView); + }); + } + + public TaskViewFactory getTaskViewFactory() { + return new TaskViewFactoryImpl(); + } + + private class TaskViewFactoryImpl implements TaskViewFactory { + @ExternalThread + public void create(@UiContext Context context, + Executor executor, Consumer<TaskView> onCreate) { + mShellExecutor.execute(() -> { + TaskViewFactoryController.this.create(context, executor, onCreate); + }); + } + } +} 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..abd92577c1d4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/WindowManagerShellWrapper.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; + +import static android.view.Display.DEFAULT_DISPLAY; + +import android.os.RemoteException; + +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.pip.PinnedStackListenerForwarder; +import com.android.wm.shell.pip.PinnedStackListenerForwarder.PinnedStackListener; + +/** + * 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(); + + /** + * Forwarder to which we can add multiple pinned stack listeners. Each listener will receive + * updates from the window manager service. + */ + private final PinnedStackListenerForwarder mPinnedStackListenerForwarder; + + public WindowManagerShellWrapper(ShellExecutor mainExecutor) { + mPinnedStackListenerForwarder = new PinnedStackListenerForwarder(mainExecutor); + } + + /** + * 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(PinnedStackListener listener) + throws RemoteException { + mPinnedStackListenerForwarder.addListener(listener); + mPinnedStackListenerForwarder.register(DEFAULT_DISPLAY); + } + + /** + * Removes a pinned stack listener. + */ + public void removePinnedStackListener(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..176c620fa119 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/FlingAnimationUtils.java @@ -0,0 +1,426 @@ +/* + * 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; + +import javax.inject.Inject; + +/** + * 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; + + @Inject + 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..8aca01d2467b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.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.animation; + +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; +import android.view.animation.PathInterpolator; + +/** + * Common interpolators used in wm shell library. + */ +public class Interpolators { + + public static final Interpolator LINEAR = new LinearInterpolator(); + + /** + * 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); + + /** + * Interpolator to be used when animating a panel closing. + */ + public static final Interpolator PANEL_CLOSE_ACCELERATED = + new PathInterpolator(0.3f, 0, 0.5f, 1); +} 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..7ea4689be7e2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt @@ -0,0 +1,1066 @@ +/* + * 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() { + 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/apppairs/AppPair.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java new file mode 100644 index 000000000000..bab5140e2f52 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java @@ -0,0 +1,281 @@ +/* + * 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.apppairs; + +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; + +import android.app.ActivityManager; +import android.graphics.Rect; +import android.view.SurfaceControl; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.split.SplitLayout; + +import java.io.PrintWriter; + +/** + * An app-pairs consisting of {@link #mRootTaskInfo} that acts as the hierarchy parent of + * {@link #mTaskInfo1} and {@link #mTaskInfo2} in the pair. + * Also includes all UI for managing the pair like the divider. + */ +class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.LayoutChangeListener { + private static final String TAG = AppPair.class.getSimpleName(); + + private ActivityManager.RunningTaskInfo mRootTaskInfo; + private SurfaceControl mRootTaskLeash; + private ActivityManager.RunningTaskInfo mTaskInfo1; + private SurfaceControl mTaskLeash1; + private ActivityManager.RunningTaskInfo mTaskInfo2; + private SurfaceControl mTaskLeash2; + + private final AppPairsController mController; + private final SyncTransactionQueue mSyncQueue; + private final DisplayController mDisplayController; + private SplitLayout mSplitLayout; + + AppPair(AppPairsController controller) { + mController = controller; + mSyncQueue = controller.getSyncTransactionQueue(); + mDisplayController = controller.getDisplayController(); + } + + int getRootTaskId() { + return mRootTaskInfo != null ? mRootTaskInfo.taskId : INVALID_TASK_ID; + } + + private int getTaskId1() { + return mTaskInfo1 != null ? mTaskInfo1.taskId : INVALID_TASK_ID; + } + + private int getTaskId2() { + return mTaskInfo2 != null ? mTaskInfo2.taskId : INVALID_TASK_ID; + } + + boolean contains(int taskId) { + return taskId == getRootTaskId() || taskId == getTaskId1() || taskId == getTaskId2(); + } + + boolean pair(ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) { + ProtoLog.v(WM_SHELL_TASK_ORG, "pair task1=%d task2=%d in AppPair=%s", + task1.taskId, task2.taskId, this); + + if (!task1.isResizeable || !task2.isResizeable) { + ProtoLog.e(WM_SHELL_TASK_ORG, + "Can't pair unresizeable tasks task1.isResizeable=%b task1.isResizeable=%b", + task1.isResizeable, task2.isResizeable); + return false; + } + + mTaskInfo1 = task1; + mTaskInfo2 = task2; + mSplitLayout = new SplitLayout(TAG + "SplitDivider", + mDisplayController.getDisplayContext(mRootTaskInfo.displayId), + mRootTaskInfo.configuration, this /* layoutChangeListener */, + b -> b.setParent(mRootTaskLeash)); + + final WindowContainerToken token1 = task1.token; + final WindowContainerToken token2 = task2.token; + final WindowContainerTransaction wct = new WindowContainerTransaction(); + + wct.setHidden(mRootTaskInfo.token, false) + .reparent(token1, mRootTaskInfo.token, true /* onTop */) + .reparent(token2, mRootTaskInfo.token, true /* onTop */) + .setWindowingMode(token1, WINDOWING_MODE_MULTI_WINDOW) + .setWindowingMode(token2, WINDOWING_MODE_MULTI_WINDOW) + .setBounds(token1, mSplitLayout.getBounds1()) + .setBounds(token2, mSplitLayout.getBounds2()) + // Moving the root task to top after the child tasks were repareted , or the root + // task cannot be visible and focused. + .reorder(mRootTaskInfo.token, true); + mController.getTaskOrganizer().applyTransaction(wct); + return true; + } + + void unpair() { + unpair(null /* toTopToken */); + } + + private void unpair(@Nullable WindowContainerToken toTopToken) { + final WindowContainerToken token1 = mTaskInfo1.token; + final WindowContainerToken token2 = mTaskInfo2.token; + final WindowContainerTransaction wct = new WindowContainerTransaction(); + + // Reparent out of this container and reset windowing mode. + wct.setHidden(mRootTaskInfo.token, true) + .reorder(mRootTaskInfo.token, false) + .reparent(token1, null, token1 == toTopToken /* onTop */) + .reparent(token2, null, token2 == toTopToken /* onTop */) + .setWindowingMode(token1, WINDOWING_MODE_UNDEFINED) + .setWindowingMode(token2, WINDOWING_MODE_UNDEFINED); + mController.getTaskOrganizer().applyTransaction(wct); + + mTaskInfo1 = null; + mTaskInfo2 = null; + mSplitLayout.release(); + mSplitLayout = null; + } + + @Override + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + if (mRootTaskInfo == null || taskInfo.taskId == mRootTaskInfo.taskId) { + mRootTaskInfo = taskInfo; + mRootTaskLeash = leash; + } else if (taskInfo.taskId == getTaskId1()) { + mTaskInfo1 = taskInfo; + mTaskLeash1 = leash; + } else if (taskInfo.taskId == getTaskId2()) { + mTaskInfo2 = taskInfo; + mTaskLeash2 = leash; + } else { + throw new IllegalStateException("Unknown task=" + taskInfo.taskId); + } + + if (mTaskLeash1 == null || mTaskLeash2 == null) return; + + mSplitLayout.init(); + final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); + final Rect dividerBounds = mSplitLayout.getDividerBounds(); + + // TODO: Is there more we need to do here? + mSyncQueue.runInSync(t -> { + t.setLayer(dividerLeash, Integer.MAX_VALUE) + .setPosition(mTaskLeash1, mTaskInfo1.positionInParent.x, + mTaskInfo1.positionInParent.y) + .setPosition(mTaskLeash2, mTaskInfo2.positionInParent.x, + mTaskInfo2.positionInParent.y) + .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top) + .show(mRootTaskLeash) + .show(mTaskLeash1) + .show(mTaskLeash2); + }); + } + + @Override + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (taskInfo.taskId == getRootTaskId()) { + if (mRootTaskInfo.isVisible != taskInfo.isVisible) { + mSyncQueue.runInSync(t -> { + if (taskInfo.isVisible) { + t.show(mRootTaskLeash); + } else { + t.hide(mRootTaskLeash); + } + }); + } + mRootTaskInfo = taskInfo; + + if (mSplitLayout != null + && mSplitLayout.updateConfiguration(mRootTaskInfo.configuration)) { + onBoundsChanged(mSplitLayout); + } + } else if (taskInfo.taskId == getTaskId1()) { + mTaskInfo1 = taskInfo; + } else if (taskInfo.taskId == getTaskId2()) { + mTaskInfo2 = taskInfo; + } else { + throw new IllegalStateException("Unknown task=" + taskInfo.taskId); + } + } + + @Override + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + if (taskInfo.taskId == getRootTaskId()) { + // We don't want to release this object back to the pool since the root task went away. + mController.unpair(mRootTaskInfo.taskId, false /* releaseToPool */); + } else if (taskInfo.taskId == getTaskId1() || taskInfo.taskId == getTaskId2()) { + mController.unpair(mRootTaskInfo.taskId); + } + } + + @Override + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + final String childPrefix = innerPrefix + " "; + pw.println(prefix + this); + pw.println(innerPrefix + "Root taskId=" + getRootTaskId() + + " winMode=" + mRootTaskInfo.getWindowingMode()); + if (mTaskInfo1 != null) { + pw.println(innerPrefix + "1 taskId=" + mTaskInfo1.taskId + + " winMode=" + mTaskInfo1.getWindowingMode()); + } + if (mTaskInfo2 != null) { + pw.println(innerPrefix + "2 taskId=" + mTaskInfo2.taskId + + " winMode=" + mTaskInfo2.getWindowingMode()); + } + } + + @Override + public String toString() { + return TAG + "#" + getRootTaskId(); + } + + @Override + public void onSnappedToDismiss(boolean snappedToEnd) { + unpair(snappedToEnd ? mTaskInfo1.token : mTaskInfo2.token /* toTopToken */); + } + + @Override + public void onBoundsChanging(SplitLayout layout) { + final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); + if (dividerLeash == null) return; + final Rect dividerBounds = layout.getDividerBounds(); + final Rect bounds1 = layout.getBounds1(); + final Rect bounds2 = layout.getBounds2(); + mSyncQueue.runInSync(t -> t + .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top) + .setPosition(mTaskLeash1, bounds1.left, bounds1.top) + .setPosition(mTaskLeash2, bounds2.left, bounds2.top) + // Sets crop to prevent visible region of tasks overlap with each other when + // re-positioning surfaces while resizing. + .setWindowCrop(mTaskLeash1, bounds1.width(), bounds1.height()) + .setWindowCrop(mTaskLeash2, bounds2.width(), bounds2.height())); + } + + @Override + public void onBoundsChanged(SplitLayout layout) { + final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); + if (dividerLeash == null) return; + final Rect dividerBounds = layout.getDividerBounds(); + final Rect bounds1 = layout.getBounds1(); + final Rect bounds2 = layout.getBounds2(); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setBounds(mTaskInfo1.token, bounds1) + .setBounds(mTaskInfo2.token, bounds2); + mController.getTaskOrganizer().applyTransaction(wct); + mSyncQueue.runInSync(t -> t + // Resets layer of divider bar to make sure it is always on top. + .setLayer(dividerLeash, Integer.MAX_VALUE) + .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top) + .setPosition(mTaskLeash1, bounds1.left, bounds1.top) + .setPosition(mTaskLeash2, bounds2.left, bounds2.top) + // Resets crop to apply new surface bounds directly. + .setWindowCrop(mTaskLeash1, null) + .setWindowCrop(mTaskLeash2, null)); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java new file mode 100644 index 000000000000..f5aa852c87ae --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.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.apppairs; + +import android.app.ActivityManager; + +import androidx.annotation.NonNull; + +import com.android.wm.shell.common.annotations.ExternalThread; + +import java.io.PrintWriter; + +/** + * Interface to engage app pairs feature. + */ +@ExternalThread +public interface AppPairs { + /** Pairs indicated tasks. */ + boolean pair(int task1, int task2); + /** Pairs indicated tasks. */ + boolean pair(ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2); + /** Unpairs any app-pair containing this task id. */ + void unpair(int taskId); + /** Dumps current status of app pairs. */ + void dump(@NonNull PrintWriter pw, String prefix); + /** Called when the shell organizer has been registered. */ + void onOrganizerRegistered(); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java new file mode 100644 index 000000000000..e380426b9ca2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java @@ -0,0 +1,222 @@ +/* + * 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.apppairs; + +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; + +import android.app.ActivityManager; +import android.util.Slog; +import android.util.SparseArray; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; + +import java.io.PrintWriter; +import java.util.concurrent.TimeUnit; + +/** + * Class manages app-pairs multitasking mode and implements the main interface {@link AppPairs}. + */ +public class AppPairsController { + private static final String TAG = AppPairsController.class.getSimpleName(); + + private final ShellTaskOrganizer mTaskOrganizer; + private final SyncTransactionQueue mSyncQueue; + private final ShellExecutor mMainExecutor; + private final AppPairsImpl mImpl = new AppPairsImpl(); + + private AppPairsPool mPairsPool; + // Active app-pairs mapped by root task id key. + private final SparseArray<AppPair> mActiveAppPairs = new SparseArray<>(); + private final DisplayController mDisplayController; + + /** + * Creates {@link AppPairs}, returns {@code null} if the feature is not supported. + */ + @Nullable + public static AppPairs create(ShellTaskOrganizer organizer, + SyncTransactionQueue syncQueue, DisplayController displayController, + ShellExecutor mainExecutor) { + return new AppPairsController(organizer, syncQueue, displayController, + mainExecutor).mImpl; + } + + AppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue, + DisplayController displayController, ShellExecutor mainExecutor) { + mTaskOrganizer = organizer; + mSyncQueue = syncQueue; + mDisplayController = displayController; + mMainExecutor = mainExecutor; + } + + void onOrganizerRegistered() { + if (mPairsPool == null) { + setPairsPool(new AppPairsPool(this)); + } + } + + @VisibleForTesting + void setPairsPool(AppPairsPool pool) { + mPairsPool = pool; + } + + boolean pair(int taskId1, int taskId2) { + final ActivityManager.RunningTaskInfo task1 = mTaskOrganizer.getRunningTaskInfo(taskId1); + final ActivityManager.RunningTaskInfo task2 = mTaskOrganizer.getRunningTaskInfo(taskId2); + if (task1 == null || task2 == null) { + return false; + } + return pair(task1, task2); + } + + boolean pair(ActivityManager.RunningTaskInfo task1, + ActivityManager.RunningTaskInfo task2) { + return pairInner(task1, task2) != null; + } + + @VisibleForTesting + AppPair pairInner( + @NonNull ActivityManager.RunningTaskInfo task1, + @NonNull ActivityManager.RunningTaskInfo task2) { + final AppPair pair = mPairsPool.acquire(); + if (!pair.pair(task1, task2)) { + mPairsPool.release(pair); + return null; + } + + mActiveAppPairs.put(pair.getRootTaskId(), pair); + return pair; + } + + void unpair(int taskId) { + unpair(taskId, true /* releaseToPool */); + } + + void unpair(int taskId, boolean releaseToPool) { + AppPair pair = mActiveAppPairs.get(taskId); + if (pair == null) { + for (int i = mActiveAppPairs.size() - 1; i >= 0; --i) { + final AppPair candidate = mActiveAppPairs.valueAt(i); + if (candidate.contains(taskId)) { + pair = candidate; + break; + } + } + } + if (pair == null) { + ProtoLog.v(WM_SHELL_TASK_ORG, "taskId %d isn't isn't in an app-pair.", taskId); + return; + } + + ProtoLog.v(WM_SHELL_TASK_ORG, "unpair taskId=%d pair=%s", taskId, pair); + mActiveAppPairs.remove(pair.getRootTaskId()); + pair.unpair(); + if (releaseToPool) { + mPairsPool.release(pair); + } + } + + ShellTaskOrganizer getTaskOrganizer() { + return mTaskOrganizer; + } + + SyncTransactionQueue getSyncTransactionQueue() { + return mSyncQueue; + } + + DisplayController getDisplayController() { + return mDisplayController; + } + + private void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + final String childPrefix = innerPrefix + " "; + pw.println(prefix + this); + + for (int i = mActiveAppPairs.size() - 1; i >= 0; --i) { + mActiveAppPairs.valueAt(i).dump(pw, childPrefix); + } + + if (mPairsPool != null) { + mPairsPool.dump(pw, prefix); + } + } + + @Override + public String toString() { + return TAG + "#" + mActiveAppPairs.size(); + } + + private class AppPairsImpl implements AppPairs { + @Override + public boolean pair(int task1, int task2) { + boolean[] result = new boolean[1]; + try { + mMainExecutor.executeBlocking(() -> { + result[0] = AppPairsController.this.pair(task1, task2); + }); + } catch (InterruptedException e) { + Slog.e(TAG, "Failed to pair tasks: " + task1 + ", " + task2); + } + return result[0]; + } + + @Override + public boolean pair(ActivityManager.RunningTaskInfo task1, + ActivityManager.RunningTaskInfo task2) { + boolean[] result = new boolean[1]; + try { + mMainExecutor.executeBlocking(() -> { + result[0] = AppPairsController.this.pair(task1, task2); + }); + } catch (InterruptedException e) { + Slog.e(TAG, "Failed to pair tasks: " + task1 + ", " + task2); + } + return result[0]; + } + + @Override + public void unpair(int taskId) { + mMainExecutor.execute(() -> { + AppPairsController.this.unpair(taskId); + }); + } + + @Override + public void onOrganizerRegistered() { + mMainExecutor.execute(() -> { + AppPairsController.this.onOrganizerRegistered(); + }); + } + + @Override + public void dump(@NonNull PrintWriter pw, String prefix) { + try { + mMainExecutor.executeBlocking(() -> AppPairsController.this.dump(pw, prefix)); + } catch (InterruptedException e) { + Slog.e(TAG, "Failed to dump AppPairsController in 2s"); + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsPool.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsPool.java new file mode 100644 index 000000000000..5c6037ea0702 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsPool.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.apppairs; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; + +import androidx.annotation.NonNull; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; + +import java.io.PrintWriter; +import java.util.ArrayList; + +/** + * Class that manager pool of {@link AppPair} objects. Helps reduce the need to call system_server + * to create a root task for the app-pair when needed since we always have one ready to go. + */ +class AppPairsPool { + private static final String TAG = AppPairsPool.class.getSimpleName(); + + @VisibleForTesting + final AppPairsController mController; + // The pool + private final ArrayList<AppPair> mPool = new ArrayList(); + + AppPairsPool(AppPairsController controller) { + mController = controller; + incrementPool(); + } + + AppPair acquire() { + final AppPair entry = mPool.remove(mPool.size() - 1); + ProtoLog.v(WM_SHELL_TASK_ORG, "acquire entry.taskId=%s listener=%s size=%d", + entry.getRootTaskId(), entry, mPool.size()); + if (mPool.size() == 0) { + incrementPool(); + } + return entry; + } + + void release(AppPair entry) { + mPool.add(entry); + ProtoLog.v(WM_SHELL_TASK_ORG, "release entry.taskId=%s listener=%s size=%d", + entry.getRootTaskId(), entry, mPool.size()); + } + + @VisibleForTesting + void incrementPool() { + ProtoLog.v(WM_SHELL_TASK_ORG, "incrementPool size=%d", mPool.size()); + final AppPair entry = new AppPair(mController); + // TODO: multi-display... + mController.getTaskOrganizer().createRootTask( + DEFAULT_DISPLAY, WINDOWING_MODE_FULLSCREEN, entry); + mPool.add(entry); + } + + @VisibleForTesting + int poolSize() { + return mPool.size(); + } + + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + final String childPrefix = innerPrefix + " "; + pw.println(prefix + this); + for (int i = mPool.size() - 1; i >= 0; --i) { + mPool.get(i).dump(pw, childPrefix); + } + } + + @Override + public String toString() { + return TAG + "#" + mPool.size(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java new file mode 100644 index 000000000000..1971ca97d426 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java @@ -0,0 +1,335 @@ +/* + * 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.bubbles; + +import static android.graphics.Paint.DITHER_FLAG; +import static android.graphics.Paint.FILTER_BITMAP_FLAG; + +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.PaintFlagsDrawFilter; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.PathParser; +import android.widget.ImageView; + +import com.android.launcher3.icons.DotRenderer; +import com.android.wm.shell.animation.Interpolators; + +import java.util.EnumSet; + +/** + * View that displays an adaptive icon with an app-badge and a dot. + * + * Dot = a small colored circle that indicates whether this bubble has an unread update. + * Badge = the icon associated with the app that created this bubble, this will show work profile + * badge if appropriate. + */ +public class BadgedImageView extends ImageView { + + /** Same value as Launcher3 dot code */ + public static final float WHITE_SCRIM_ALPHA = 0.54f; + /** Same as value in Launcher3 IconShape */ + public static final int DEFAULT_PATH_SIZE = 100; + /** Same as value in Launcher3 BaseIconFactory */ + private static final float ICON_BADGE_SCALE = 0.444f; + + /** + * Flags that suppress the visibility of the 'new' dot, for one reason or another. If any of + * these flags are set, the dot will not be shown even if {@link Bubble#showDot()} returns true. + */ + enum SuppressionFlag { + // Suppressed because the flyout is visible - it will morph into the dot via animation. + FLYOUT_VISIBLE, + // Suppressed because this bubble is behind others in the collapsed stack. + BEHIND_STACK, + } + + /** + * Start by suppressing the dot because the flyout is visible - most bubbles are added with a + * flyout, so this is a reasonable default. + */ + private final EnumSet<SuppressionFlag> mDotSuppressionFlags = + EnumSet.of(SuppressionFlag.FLYOUT_VISIBLE); + + private float mDotScale = 0f; + private float mAnimatingToDotScale = 0f; + private boolean mDotIsAnimating = false; + + private BubbleViewProvider mBubble; + private BubblePositioner mPositioner; + + private DotRenderer mDotRenderer; + private DotRenderer.DrawParams mDrawParams; + private boolean mOnLeft; + + private int mDotColor; + + private Rect mTempBounds = new Rect(); + + public BadgedImageView(Context context) { + this(context, null); + } + + public BadgedImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + mDrawParams = new DotRenderer.DrawParams(); + + setFocusable(true); + setClickable(true); + } + + public void initialize(BubblePositioner positioner) { + mPositioner = positioner; + + Path iconPath = PathParser.createPathFromPathData( + getResources().getString(com.android.internal.R.string.config_icon_mask)); + mDotRenderer = new DotRenderer(mPositioner.getBubbleBitmapSize(), + iconPath, DEFAULT_PATH_SIZE); + } + + public void showDotAndBadge(boolean onLeft) { + removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.BEHIND_STACK); + animateDotBadgePositions(onLeft); + + } + + public void hideDotAndBadge(boolean onLeft) { + addDotSuppressionFlag(BadgedImageView.SuppressionFlag.BEHIND_STACK); + mOnLeft = onLeft; + hideBadge(); + } + + /** + * Updates the view with provided info. + */ + public void setRenderedBubble(BubbleViewProvider bubble) { + mBubble = bubble; + if (mDotSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK)) { + hideBadge(); + } else { + showBadge(); + } + mDotColor = bubble.getDotColor(); + drawDot(bubble.getDotPath()); + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (!shouldDrawDot()) { + return; + } + + getDrawingRect(mTempBounds); + + mDrawParams.color = mDotColor; + mDrawParams.iconBounds = mTempBounds; + mDrawParams.leftAlign = mOnLeft; + mDrawParams.scale = mDotScale; + + mDotRenderer.draw(canvas, mDrawParams); + } + + /** Adds a dot suppression flag, updating dot visibility if needed. */ + void addDotSuppressionFlag(SuppressionFlag flag) { + if (mDotSuppressionFlags.add(flag)) { + // Update dot visibility, and animate out if we're now behind the stack. + updateDotVisibility(flag == SuppressionFlag.BEHIND_STACK /* animate */); + } + } + + /** Removes a dot suppression flag, updating dot visibility if needed. */ + void removeDotSuppressionFlag(SuppressionFlag flag) { + if (mDotSuppressionFlags.remove(flag)) { + // Update dot visibility, animating if we're no longer behind the stack. + updateDotVisibility(flag == SuppressionFlag.BEHIND_STACK); + } + } + + /** Updates the visibility of the dot, animating if requested. */ + void updateDotVisibility(boolean animate) { + final float targetScale = shouldDrawDot() ? 1f : 0f; + + if (animate) { + animateDotScale(targetScale, null /* after */); + } else { + mDotScale = targetScale; + mAnimatingToDotScale = targetScale; + invalidate(); + } + } + + /** + * @param iconPath The new icon path to use when calculating dot position. + */ + void drawDot(Path iconPath) { + mDotRenderer = new DotRenderer(mPositioner.getBubbleBitmapSize(), + iconPath, DEFAULT_PATH_SIZE); + invalidate(); + } + + /** + * How big the dot should be, fraction from 0 to 1. + */ + void setDotScale(float fraction) { + mDotScale = fraction; + invalidate(); + } + + /** + * Whether decorations (badges or dots) are on the left. + */ + boolean getDotOnLeft() { + return mOnLeft; + } + + /** + * Return dot position relative to bubble view container bounds. + */ + float[] getDotCenter() { + float[] dotPosition; + if (mOnLeft) { + dotPosition = mDotRenderer.getLeftDotPosition(); + } else { + dotPosition = mDotRenderer.getRightDotPosition(); + } + getDrawingRect(mTempBounds); + float dotCenterX = mTempBounds.width() * dotPosition[0]; + float dotCenterY = mTempBounds.height() * dotPosition[1]; + return new float[]{dotCenterX, dotCenterY}; + } + + /** + * The key for the {@link Bubble} associated with this view, if one exists. + */ + @Nullable + public String getKey() { + return (mBubble != null) ? mBubble.getKey() : null; + } + + int getDotColor() { + return mDotColor; + } + + /** Sets the position of the dot and badge, animating them out and back in if requested. */ + void animateDotBadgePositions(boolean onLeft) { + mOnLeft = onLeft; + + if (onLeft != getDotOnLeft() && shouldDrawDot()) { + animateDotScale(0f /* showDot */, () -> { + invalidate(); + animateDotScale(1.0f, null /* after */); + }); + } + // TODO animate badge + showBadge(); + + } + + /** Sets the position of the dot and badge. */ + void setDotBadgeOnLeft(boolean onLeft) { + mOnLeft = onLeft; + invalidate(); + showBadge(); + } + + + /** Whether to draw the dot in onDraw(). */ + private boolean shouldDrawDot() { + // Always render the dot if it's animating, since it could be animating out. Otherwise, show + // it if the bubble wants to show it, and we aren't suppressing it. + return mDotIsAnimating || (mBubble.showDot() && mDotSuppressionFlags.isEmpty()); + } + + /** + * Animates the dot to the given scale, running the optional callback when the animation ends. + */ + private void animateDotScale(float toScale, @Nullable Runnable after) { + mDotIsAnimating = true; + + // Don't restart the animation if we're already animating to the given value. + if (mAnimatingToDotScale == toScale || !shouldDrawDot()) { + mDotIsAnimating = false; + return; + } + + mAnimatingToDotScale = toScale; + + final boolean showDot = toScale > 0f; + + // Do NOT wait until after animation ends to setShowDot + // to avoid overriding more recent showDot states. + clearAnimation(); + animate() + .setDuration(200) + .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) + .setUpdateListener((valueAnimator) -> { + float fraction = valueAnimator.getAnimatedFraction(); + fraction = showDot ? fraction : 1f - fraction; + setDotScale(fraction); + }).withEndAction(() -> { + setDotScale(showDot ? 1f : 0f); + mDotIsAnimating = false; + if (after != null) { + after.run(); + } + }).start(); + } + + void showBadge() { + Drawable badge = mBubble.getAppBadge(); + if (badge == null) { + setImageBitmap(mBubble.getBubbleIcon()); + return; + } + Canvas bubbleCanvas = new Canvas(); + Bitmap noBadgeBubble = mBubble.getBubbleIcon(); + Bitmap bubble = noBadgeBubble.copy(noBadgeBubble.getConfig(), /* isMutable */ true); + + bubbleCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG)); + bubbleCanvas.setBitmap(bubble); + final int bubbleSize = bubble.getWidth(); + final int badgeSize = (int) (ICON_BADGE_SCALE * bubbleSize); + if (mOnLeft) { + badge.setBounds(0, bubbleSize - badgeSize, badgeSize, bubbleSize); + } else { + badge.setBounds(bubbleSize - badgeSize, bubbleSize - badgeSize, + bubbleSize, bubbleSize); + } + badge.draw(bubbleCanvas); + bubbleCanvas.setBitmap(null); + setImageBitmap(bubble); + } + + void hideBadge() { + setImageBitmap(mBubble.getBubbleIcon()); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java new file mode 100644 index 000000000000..ffeabd876b81 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -0,0 +1,827 @@ +/* + * 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.bubbles; + +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.os.AsyncTask.Status.FINISHED; + +import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; + +import android.annotation.DimenRes; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Notification; +import android.app.PendingIntent; +import android.app.Person; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ShortcutInfo; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Path; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.os.Parcelable; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.logging.InstanceId; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * Encapsulates the data and UI elements of a bubble. + */ +@VisibleForTesting +public class Bubble implements BubbleViewProvider { + private static final String TAG = "Bubble"; + + private final String mKey; + private final Executor mMainExecutor; + + private long mLastUpdated; + private long mLastAccessed; + + @Nullable + private Bubbles.NotificationSuppressionChangedListener mSuppressionListener; + + /** Whether the bubble should show a dot for the notification indicating updated content. */ + private boolean mShowBubbleUpdateDot = true; + + /** Whether flyout text should be suppressed, regardless of any other flags or state. */ + private boolean mSuppressFlyout; + + // Items that are typically loaded later + private String mAppName; + private ShortcutInfo mShortcutInfo; + private String mMetadataShortcutId; + private BadgedImageView mIconView; + private BubbleExpandedView mExpandedView; + + private BubbleViewInfoTask mInflationTask; + private boolean mInflateSynchronously; + private boolean mPendingIntentCanceled; + private boolean mIsImportantConversation; + + /** + * Presentational info about the flyout. + */ + public static class FlyoutMessage { + @Nullable public Icon senderIcon; + @Nullable public Drawable senderAvatar; + @Nullable public CharSequence senderName; + @Nullable public CharSequence message; + @Nullable public boolean isGroupChat; + } + + private FlyoutMessage mFlyoutMessage; + private Drawable mBadgeDrawable; + // Bitmap with no badge, no dot + private Bitmap mBubbleBitmap; + private int mDotColor; + private Path mDotPath; + private int mFlags; + + @NonNull + private UserHandle mUser; + @NonNull + private String mPackageName; + @Nullable + private String mTitle; + @Nullable + private Icon mIcon; + private boolean mIsBubble; + private boolean mIsVisuallyInterruptive; + private boolean mIsClearable; + private boolean mShouldSuppressNotificationDot; + private boolean mShouldSuppressNotificationList; + private boolean mShouldSuppressPeek; + private int mDesiredHeight; + @DimenRes + private int mDesiredHeightResId; + + /** for logging **/ + @Nullable + private InstanceId mInstanceId; + @Nullable + private String mChannelId; + private int mNotificationId; + private int mAppUid = -1; + + /** + * A bubble is created and can be updated. This intent is updated until the user first + * expands the bubble. Once the user has expanded the contents, we ignore the intent updates + * to prevent restarting the intent & possibly altering UI state in the activity in front of + * the user. + * + * Once the bubble is overflowed, the activity is finished and updates to the + * notification are respected. Typically an update to an overflowed bubble would result in + * that bubble being added back to the stack anyways. + */ + @Nullable + private PendingIntent mIntent; + private boolean mIntentActive; + @Nullable + private PendingIntent.CancelListener mIntentCancelListener; + + /** + * Sent when the bubble & notification are no longer visible to the user (i.e. no + * notification in the shade, no bubble in the stack or overflow). + */ + @Nullable + private PendingIntent mDeleteIntent; + + /** + * Create a bubble with limited information based on given {@link ShortcutInfo}. + * Note: Currently this is only being used when the bubble is persisted to disk. + */ + Bubble(@NonNull final String key, @NonNull final ShortcutInfo shortcutInfo, + final int desiredHeight, final int desiredHeightResId, @Nullable final String title, + Executor mainExecutor) { + Objects.requireNonNull(key); + Objects.requireNonNull(shortcutInfo); + mMetadataShortcutId = shortcutInfo.getId(); + mShortcutInfo = shortcutInfo; + mKey = key; + mFlags = 0; + mUser = shortcutInfo.getUserHandle(); + mPackageName = shortcutInfo.getPackage(); + mIcon = shortcutInfo.getIcon(); + mDesiredHeight = desiredHeight; + mDesiredHeightResId = desiredHeightResId; + mTitle = title; + mShowBubbleUpdateDot = false; + mMainExecutor = mainExecutor; + } + + @VisibleForTesting(visibility = PRIVATE) + Bubble(@NonNull final BubbleEntry entry, + @Nullable final Bubbles.NotificationSuppressionChangedListener listener, + final Bubbles.PendingIntentCanceledListener intentCancelListener, + Executor mainExecutor) { + mKey = entry.getKey(); + mSuppressionListener = listener; + mIntentCancelListener = intent -> { + if (mIntent != null) { + mIntent.unregisterCancelListener(mIntentCancelListener); + } + mainExecutor.execute(() -> { + intentCancelListener.onPendingIntentCanceled(this); + }); + }; + mMainExecutor = mainExecutor; + setEntry(entry); + } + + @Override + public String getKey() { + return mKey; + } + + public UserHandle getUser() { + return mUser; + } + + @NonNull + public String getPackageName() { + return mPackageName; + } + + @Override + public Bitmap getBubbleIcon() { + return mBubbleBitmap; + } + + @Override + public Drawable getAppBadge() { + return mBadgeDrawable; + } + + @Override + public int getDotColor() { + return mDotColor; + } + + @Override + public Path getDotPath() { + return mDotPath; + } + + @Nullable + public String getAppName() { + return mAppName; + } + + @Nullable + public ShortcutInfo getShortcutInfo() { + return mShortcutInfo; + } + + @Nullable + @Override + public BadgedImageView getIconView() { + return mIconView; + } + + @Override + @Nullable + public BubbleExpandedView getExpandedView() { + return mExpandedView; + } + + @Nullable + public String getTitle() { + return mTitle; + } + + String getMetadataShortcutId() { + return mMetadataShortcutId; + } + + boolean hasMetadataShortcutId() { + return (mMetadataShortcutId != null && !mMetadataShortcutId.isEmpty()); + } + + /** + * Call this to clean up the task for the bubble. Ensure this is always called when done with + * the bubble. + */ + void cleanupExpandedView() { + if (mExpandedView != null) { + mExpandedView.cleanUpExpandedState(); + mExpandedView = null; + } + if (mIntent != null) { + mIntent.unregisterCancelListener(mIntentCancelListener); + } + mIntentActive = false; + } + + /** + * Call when all the views should be removed/cleaned up. + */ + void cleanupViews() { + cleanupExpandedView(); + mIconView = null; + } + + void setPendingIntentCanceled() { + mPendingIntentCanceled = true; + } + + boolean getPendingIntentCanceled() { + return mPendingIntentCanceled; + } + + /** + * Sets whether to perform inflation on the same thread as the caller. This method should only + * be used in tests, not in production. + */ + @VisibleForTesting + void setInflateSynchronously(boolean inflateSynchronously) { + mInflateSynchronously = inflateSynchronously; + } + + /** + * Sets whether this bubble is considered visually interruptive. This method is purely for + * testing. + */ + @VisibleForTesting + void setVisuallyInterruptiveForTest(boolean visuallyInterruptive) { + mIsVisuallyInterruptive = visuallyInterruptive; + } + + /** + * Starts a task to inflate & load any necessary information to display a bubble. + * + * @param callback the callback to notify one the bubble is ready to be displayed. + * @param context the context for the bubble. + * @param controller the bubble controller. + * @param stackView the stackView the bubble is eventually added to. + * @param iconFactory the iconfactory use to create badged images for the bubble. + */ + void inflate(BubbleViewInfoTask.Callback callback, + Context context, + BubbleController controller, + BubbleStackView stackView, + BubbleIconFactory iconFactory, + boolean skipInflation) { + if (isBubbleLoading()) { + mInflationTask.cancel(true /* mayInterruptIfRunning */); + } + mInflationTask = new BubbleViewInfoTask(this, + context, + controller, + stackView, + iconFactory, + skipInflation, + callback, + mMainExecutor); + if (mInflateSynchronously) { + mInflationTask.onPostExecute(mInflationTask.doInBackground()); + } else { + mInflationTask.execute(); + } + } + + private boolean isBubbleLoading() { + return mInflationTask != null && mInflationTask.getStatus() != FINISHED; + } + + boolean isInflated() { + return mIconView != null && mExpandedView != null; + } + + void stopInflation() { + if (mInflationTask == null) { + return; + } + mInflationTask.cancel(true /* mayInterruptIfRunning */); + } + + void setViewInfo(BubbleViewInfoTask.BubbleViewInfo info) { + if (!isInflated()) { + mIconView = info.imageView; + mExpandedView = info.expandedView; + } + + mShortcutInfo = info.shortcutInfo; + mAppName = info.appName; + mFlyoutMessage = info.flyoutMessage; + + mBadgeDrawable = info.badgeDrawable; + mBubbleBitmap = info.bubbleBitmap; + + mDotColor = info.dotColor; + mDotPath = info.dotPath; + + if (mExpandedView != null) { + mExpandedView.update(this /* bubble */); + } + if (mIconView != null) { + mIconView.setRenderedBubble(this /* bubble */); + } + } + + /** + * Set visibility of bubble in the expanded state. + * + * @param visibility {@code true} if the expanded bubble should be visible on the screen. + * + * Note that this contents visibility doesn't affect visibility at {@link android.view.View}, + * and setting {@code false} actually means rendering the expanded view in transparent. + */ + @Override + public void setContentVisibility(boolean visibility) { + if (mExpandedView != null) { + mExpandedView.setContentVisibility(visibility); + } + } + + /** + * Sets the entry associated with this bubble. + */ + void setEntry(@NonNull final BubbleEntry entry) { + Objects.requireNonNull(entry); + mLastUpdated = entry.getStatusBarNotification().getPostTime(); + mIsBubble = entry.getStatusBarNotification().getNotification().isBubbleNotification(); + mPackageName = entry.getStatusBarNotification().getPackageName(); + mUser = entry.getStatusBarNotification().getUser(); + mTitle = getTitle(entry); + mChannelId = entry.getStatusBarNotification().getNotification().getChannelId(); + mNotificationId = entry.getStatusBarNotification().getId(); + mAppUid = entry.getStatusBarNotification().getUid(); + mInstanceId = entry.getStatusBarNotification().getInstanceId(); + mFlyoutMessage = extractFlyoutMessage(entry); + if (entry.getRanking() != null) { + mShortcutInfo = entry.getRanking().getConversationShortcutInfo(); + mIsVisuallyInterruptive = entry.getRanking().visuallyInterruptive(); + if (entry.getRanking().getChannel() != null) { + mIsImportantConversation = + entry.getRanking().getChannel().isImportantConversation(); + } + } + if (entry.getBubbleMetadata() != null) { + mMetadataShortcutId = entry.getBubbleMetadata().getShortcutId(); + mFlags = entry.getBubbleMetadata().getFlags(); + mDesiredHeight = entry.getBubbleMetadata().getDesiredHeight(); + mDesiredHeightResId = entry.getBubbleMetadata().getDesiredHeightResId(); + mIcon = entry.getBubbleMetadata().getIcon(); + + if (!mIntentActive || mIntent == null) { + if (mIntent != null) { + mIntent.unregisterCancelListener(mIntentCancelListener); + } + mIntent = entry.getBubbleMetadata().getIntent(); + if (mIntent != null) { + mIntent.registerCancelListener(mIntentCancelListener); + } + } else if (mIntent != null && entry.getBubbleMetadata().getIntent() == null) { + // Was an intent bubble now it's a shortcut bubble... still unregister the listener + mIntent.unregisterCancelListener(mIntentCancelListener); + mIntentActive = false; + mIntent = null; + } + mDeleteIntent = entry.getBubbleMetadata().getDeleteIntent(); + } + + mIsClearable = entry.isClearable(); + mShouldSuppressNotificationDot = entry.shouldSuppressNotificationDot(); + mShouldSuppressNotificationList = entry.shouldSuppressNotificationList(); + mShouldSuppressPeek = entry.shouldSuppressPeek(); + } + + @Nullable + Icon getIcon() { + return mIcon; + } + + boolean isVisuallyInterruptive() { + return mIsVisuallyInterruptive; + } + + /** + * @return the last time this bubble was updated or accessed, whichever is most recent. + */ + long getLastActivity() { + return Math.max(mLastUpdated, mLastAccessed); + } + + /** + * Sets if the intent used for this bubble is currently active (i.e. populating an + * expanded view, expanded or not). + */ + void setIntentActive() { + mIntentActive = true; + } + + boolean isIntentActive() { + return mIntentActive; + } + + public InstanceId getInstanceId() { + return mInstanceId; + } + + @Nullable + public String getChannelId() { + return mChannelId; + } + + public int getNotificationId() { + return mNotificationId; + } + + /** + * @return the task id of the task in which bubble contents is drawn. + */ + @Override + public int getTaskId() { + return mExpandedView != null ? mExpandedView.getTaskId() : INVALID_TASK_ID; + } + + /** + * Should be invoked whenever a Bubble is accessed (selected while expanded). + */ + void markAsAccessedAt(long lastAccessedMillis) { + mLastAccessed = lastAccessedMillis; + setSuppressNotification(true); + setShowDot(false /* show */); + } + + /** + * Should be invoked whenever a Bubble is promoted from overflow. + */ + void markUpdatedAt(long lastAccessedMillis) { + mLastUpdated = lastAccessedMillis; + } + + /** + * Whether this notification should be shown in the shade. + */ + boolean showInShade() { + return !shouldSuppressNotification() || !mIsClearable; + } + + /** + * Whether this notification conversation is important. + */ + boolean isImportantConversation() { + return mIsImportantConversation; + } + + /** + * Sets whether this notification should be suppressed in the shade. + */ + @VisibleForTesting + public void setSuppressNotification(boolean suppressNotification) { + boolean prevShowInShade = showInShade(); + if (suppressNotification) { + mFlags |= Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; + } else { + mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; + } + + if (showInShade() != prevShowInShade && mSuppressionListener != null) { + mSuppressionListener.onBubbleNotificationSuppressionChange(this); + } + } + + /** + * Sets whether the bubble for this notification should show a dot indicating updated content. + */ + void setShowDot(boolean showDot) { + mShowBubbleUpdateDot = showDot; + + if (mIconView != null) { + mIconView.updateDotVisibility(true /* animate */); + } + } + + /** + * Whether the bubble for this notification should show a dot indicating updated content. + */ + @Override + public boolean showDot() { + return mShowBubbleUpdateDot + && !mShouldSuppressNotificationDot + && !shouldSuppressNotification(); + } + + /** + * Whether the flyout for the bubble should be shown. + */ + @VisibleForTesting + public boolean showFlyout() { + return !mSuppressFlyout && !mShouldSuppressPeek + && !shouldSuppressNotification() + && !mShouldSuppressNotificationList; + } + + /** + * Set whether the flyout text for the bubble should be shown when an update is received. + * + * @param suppressFlyout whether the flyout text is shown + */ + void setSuppressFlyout(boolean suppressFlyout) { + mSuppressFlyout = suppressFlyout; + } + + FlyoutMessage getFlyoutMessage() { + return mFlyoutMessage; + } + + int getRawDesiredHeight() { + return mDesiredHeight; + } + + int getRawDesiredHeightResId() { + return mDesiredHeightResId; + } + + float getDesiredHeight(Context context) { + boolean useRes = mDesiredHeightResId != 0; + if (useRes) { + return getDimenForPackageUser(context, mDesiredHeightResId, mPackageName, + mUser.getIdentifier()); + } else { + return mDesiredHeight * context.getResources().getDisplayMetrics().density; + } + } + + String getDesiredHeightString() { + boolean useRes = mDesiredHeightResId != 0; + if (useRes) { + return String.valueOf(mDesiredHeightResId); + } else { + return String.valueOf(mDesiredHeight); + } + } + + @Nullable + PendingIntent getBubbleIntent() { + return mIntent; + } + + @Nullable + PendingIntent getDeleteIntent() { + return mDeleteIntent; + } + + Intent getSettingsIntent(final Context context) { + final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS); + intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName()); + final int uid = getUid(context); + if (uid != -1) { + intent.putExtra(Settings.EXTRA_APP_UID, uid); + } + intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + return intent; + } + + public int getAppUid() { + return mAppUid; + } + + private int getUid(final Context context) { + if (mAppUid != -1) return mAppUid; + final PackageManager pm = context.getPackageManager(); + if (pm == null) return -1; + try { + final ApplicationInfo info = pm.getApplicationInfo(mShortcutInfo.getPackage(), 0); + return info.uid; + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "cannot find uid", e); + } + return -1; + } + + private int getDimenForPackageUser(Context context, int resId, String pkg, int userId) { + Resources r; + if (pkg != null) { + try { + if (userId == UserHandle.USER_ALL) { + userId = UserHandle.USER_SYSTEM; + } + r = context.createContextAsUser(UserHandle.of(userId), /* flags */ 0) + .getPackageManager().getResourcesForApplication(pkg); + return r.getDimensionPixelSize(resId); + } catch (PackageManager.NameNotFoundException ex) { + // Uninstalled, don't care + } catch (Resources.NotFoundException e) { + // Invalid res id, return 0 and user our default + Log.e(TAG, "Couldn't find desired height res id", e); + } + } + return 0; + } + + private boolean shouldSuppressNotification() { + return isEnabled(Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION); + } + + public boolean shouldAutoExpand() { + return isEnabled(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); + } + + void setShouldAutoExpand(boolean shouldAutoExpand) { + if (shouldAutoExpand) { + enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); + } else { + disable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); + } + } + + public void setIsBubble(final boolean isBubble) { + mIsBubble = isBubble; + } + + public boolean isBubble() { + return mIsBubble; + } + + public void enable(int option) { + mFlags |= option; + } + + public void disable(int option) { + mFlags &= ~option; + } + + public boolean isEnabled(int option) { + return (mFlags & option) != 0; + } + + @Override + public String toString() { + return "Bubble{" + mKey + '}'; + } + + /** + * Description of current bubble state. + */ + public void dump( + @NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { + pw.print("key: "); pw.println(mKey); + pw.print(" showInShade: "); pw.println(showInShade()); + pw.print(" showDot: "); pw.println(showDot()); + pw.print(" showFlyout: "); pw.println(showFlyout()); + pw.print(" lastActivity: "); pw.println(getLastActivity()); + pw.print(" desiredHeight: "); pw.println(getDesiredHeightString()); + pw.print(" suppressNotif: "); pw.println(shouldSuppressNotification()); + pw.print(" autoExpand: "); pw.println(shouldAutoExpand()); + if (mExpandedView != null) { + mExpandedView.dump(fd, pw, args); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Bubble)) return false; + Bubble bubble = (Bubble) o; + return Objects.equals(mKey, bubble.mKey); + } + + @Override + public int hashCode() { + return Objects.hash(mKey); + } + + @Nullable + private static String getTitle(@NonNull final BubbleEntry e) { + final CharSequence titleCharSeq = e.getStatusBarNotification() + .getNotification().extras.getCharSequence(Notification.EXTRA_TITLE); + return titleCharSeq == null ? null : titleCharSeq.toString(); + } + + /** + * Returns our best guess for the most relevant text summary of the latest update to this + * notification, based on its type. Returns null if there should not be an update message. + */ + @NonNull + static Bubble.FlyoutMessage extractFlyoutMessage(BubbleEntry entry) { + Objects.requireNonNull(entry); + final Notification underlyingNotif = entry.getStatusBarNotification().getNotification(); + final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle(); + + Bubble.FlyoutMessage bubbleMessage = new Bubble.FlyoutMessage(); + bubbleMessage.isGroupChat = underlyingNotif.extras.getBoolean( + Notification.EXTRA_IS_GROUP_CONVERSATION); + try { + if (Notification.BigTextStyle.class.equals(style)) { + // Return the big text, it is big so probably important. If it's not there use the + // normal text. + CharSequence bigText = + underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT); + bubbleMessage.message = !TextUtils.isEmpty(bigText) + ? bigText + : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); + return bubbleMessage; + } else if (Notification.MessagingStyle.class.equals(style)) { + final List<Notification.MessagingStyle.Message> messages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray( + (Parcelable[]) underlyingNotif.extras.get( + Notification.EXTRA_MESSAGES)); + + final Notification.MessagingStyle.Message latestMessage = + Notification.MessagingStyle.findLatestIncomingMessage(messages); + if (latestMessage != null) { + bubbleMessage.message = latestMessage.getText(); + Person sender = latestMessage.getSenderPerson(); + bubbleMessage.senderName = sender != null ? sender.getName() : null; + bubbleMessage.senderAvatar = null; + bubbleMessage.senderIcon = sender != null ? sender.getIcon() : null; + return bubbleMessage; + } + } else if (Notification.InboxStyle.class.equals(style)) { + CharSequence[] lines = + underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES); + + // Return the last line since it should be the most recent. + if (lines != null && lines.length > 0) { + bubbleMessage.message = lines[lines.length - 1]; + return bubbleMessage; + } + } else if (Notification.MediaStyle.class.equals(style)) { + // Return nothing, media updates aren't typically useful as a text update. + return bubbleMessage; + } else { + // Default to text extra. + bubbleMessage.message = + underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); + return bubbleMessage; + } + } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) { + // No use crashing, we'll just return null and the caller will assume there's no update + // message. + e.printStackTrace(); + } + + return bubbleMessage; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java new file mode 100644 index 000000000000..2b53257e2774 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -0,0 +1,1401 @@ +/* + * 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.bubbles; + +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.service.notification.NotificationListenerService.REASON_CANCEL; +import static android.view.View.INVISIBLE; +import static android.view.View.VISIBLE; +import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + +import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_BOTTOM; +import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_LEFT; +import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_NONE; +import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_RIGHT; +import static com.android.wm.shell.bubbles.Bubbles.DISMISS_AGED; +import static com.android.wm.shell.bubbles.Bubbles.DISMISS_BLOCKED; +import static com.android.wm.shell.bubbles.Bubbles.DISMISS_GROUP_CANCELLED; +import static com.android.wm.shell.bubbles.Bubbles.DISMISS_INVALID_INTENT; +import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NOTIF_CANCEL; +import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NO_BUBBLE_UP; +import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NO_LONGER_BUBBLE; +import static com.android.wm.shell.bubbles.Bubbles.DISMISS_PACKAGE_REMOVED; +import static com.android.wm.shell.bubbles.Bubbles.DISMISS_SHORTCUT_REMOVED; +import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_CHANGED; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.LauncherApps; +import android.content.pm.PackageManager; +import android.content.pm.ShortcutInfo; +import android.content.res.Configuration; +import android.graphics.PixelFormat; +import android.graphics.PointF; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.service.notification.NotificationListenerService; +import android.service.notification.NotificationListenerService.RankingMap; +import android.util.ArraySet; +import android.util.Log; +import android.util.Pair; +import android.util.Slog; +import android.util.SparseSetArray; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; + +import androidx.annotation.MainThread; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.logging.UiEventLogger; +import com.android.internal.statusbar.IStatusBarService; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.WindowManagerShellWrapper; +import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.pip.PinnedStackListenerForwarder; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +/** + * Bubbles are a special type of content that can "float" on top of other apps or System UI. + * Bubbles can be expanded to show more content. + * + * The controller manages addition, removal, and visible state of bubbles on screen. + */ +public class BubbleController { + + private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; + + // TODO(b/173386799) keep in sync with Launcher3 and also don't do a broadcast + public static final String TASKBAR_CHANGED_BROADCAST = "taskbarChanged"; + public static final String EXTRA_TASKBAR_CREATED = "taskbarCreated"; + public static final String EXTRA_BUBBLE_OVERFLOW_OPENED = "bubbleOverflowOpened"; + public static final String EXTRA_TASKBAR_VISIBLE = "taskbarVisible"; + public static final String EXTRA_TASKBAR_POSITION = "taskbarPosition"; + public static final String EXTRA_TASKBAR_ICON_SIZE = "taskbarIconSize"; + public static final String EXTRA_TASKBAR_BUBBLE_XY = "taskbarBubbleXY"; + public static final String EXTRA_TASKBAR_SIZE = "taskbarSize"; + public static final String LEFT_POSITION = "Left"; + public static final String RIGHT_POSITION = "Right"; + public static final String BOTTOM_POSITION = "Bottom"; + + private final Context mContext; + private final BubblesImpl mImpl = new BubblesImpl(); + private Bubbles.BubbleExpandListener mExpandListener; + @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer; + private final FloatingContentCoordinator mFloatingContentCoordinator; + private final BubbleDataRepository mDataRepository; + private BubbleLogger mLogger; + private BubbleData mBubbleData; + private View mBubbleScrim; + @Nullable private BubbleStackView mStackView; + private BubbleIconFactory mBubbleIconFactory; + private BubblePositioner mBubblePositioner; + private Bubbles.SysuiProxy mSysuiProxy; + + // Tracks the id of the current (foreground) user. + private int mCurrentUserId; + // Saves notification keys of active bubbles when users are switched. + private final SparseSetArray<String> mSavedBubbleKeysPerUser; + + // Used when ranking updates occur and we check if things should bubble / unbubble + private NotificationListenerService.Ranking mTmpRanking; + + // Callback that updates BubbleOverflowActivity on data change. + @Nullable private BubbleData.Listener mOverflowListener = null; + + // Only load overflow data from disk once + private boolean mOverflowDataLoaded = false; + + /** + * When the shade status changes to SHADE (from anything but SHADE, like LOCKED) we'll select + * this bubble and expand the stack. + */ + @Nullable private BubbleEntry mNotifEntryToExpandOnShadeUnlock; + + private IStatusBarService mBarService; + private WindowManager mWindowManager; + + // Used to post to main UI thread + private final ShellExecutor mMainExecutor; + + /** LayoutParams used to add the BubbleStackView to the window manager. */ + private WindowManager.LayoutParams mWmLayoutParams; + /** Whether or not the BubbleStackView has been added to the WindowManager. */ + private boolean mAddedToWindowManager = false; + + /** Last known orientation, used to detect orientation changes in {@link #onConfigChanged}. */ + private int mOrientation = Configuration.ORIENTATION_UNDEFINED; + + /** + * Last known screen density, used to detect display size changes in {@link #onConfigChanged}. + */ + private int mDensityDpi = Configuration.DENSITY_DPI_UNDEFINED; + + /** + * Last known font scale, used to detect font size changes in {@link #onConfigChanged}. + */ + private float mFontScale = 0; + + /** Last known direction, used to detect layout direction changes @link #onConfigChanged}. */ + private int mLayoutDirection = View.LAYOUT_DIRECTION_UNDEFINED; + + private boolean mInflateSynchronously; + + private ShellTaskOrganizer mTaskOrganizer; + + /** + * Whether the IME is visible, as reported by the BubbleStackView. If it is, we'll make the + * Bubbles window NOT_FOCUSABLE so that touches on the Bubbles UI doesn't steal focus from the + * ActivityView and hide the IME. + */ + private boolean mImeVisible = false; + + /** true when user is in status bar unlock shade. */ + private boolean mIsStatusBarShade = true; + + /** + * Injected constructor. + */ + public static Bubbles create(Context context, + @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, + FloatingContentCoordinator floatingContentCoordinator, + @Nullable IStatusBarService statusBarService, + WindowManager windowManager, + WindowManagerShellWrapper windowManagerShellWrapper, + LauncherApps launcherApps, + UiEventLogger uiEventLogger, + ShellTaskOrganizer organizer, + ShellExecutor mainExecutor, + Handler mainHandler) { + BubbleLogger logger = new BubbleLogger(uiEventLogger); + BubblePositioner positioner = new BubblePositioner(context, windowManager); + BubbleData data = new BubbleData(context, logger, positioner, mainExecutor); + return new BubbleController(context, data, synchronizer, floatingContentCoordinator, + new BubbleDataRepository(context, launcherApps, mainExecutor), + statusBarService, windowManager, windowManagerShellWrapper, launcherApps, + logger, organizer, positioner, mainExecutor, mainHandler).mImpl; + } + + /** + * Testing constructor. + */ + @VisibleForTesting + public BubbleController(Context context, + BubbleData data, + @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, + FloatingContentCoordinator floatingContentCoordinator, + BubbleDataRepository dataRepository, + @Nullable IStatusBarService statusBarService, + WindowManager windowManager, + WindowManagerShellWrapper windowManagerShellWrapper, + LauncherApps launcherApps, + BubbleLogger bubbleLogger, + ShellTaskOrganizer organizer, + BubblePositioner positioner, + ShellExecutor mainExecutor, + Handler mainHandler) { + mContext = context; + mFloatingContentCoordinator = floatingContentCoordinator; + mDataRepository = dataRepository; + mLogger = bubbleLogger; + mMainExecutor = mainExecutor; + + mBubblePositioner = positioner; + mBubbleData = data; + mBubbleData.setListener(mBubbleDataListener); + mBubbleData.setSuppressionChangedListener(bubble -> { + // Make sure NoMan knows it's not showing in the shade anymore so anyone querying it + // can tell. + try { + mBarService.onBubbleNotificationSuppressionChanged(bubble.getKey(), + !bubble.showInShade()); + } catch (RemoteException e) { + // Bad things have happened + } + }); + mBubbleData.setPendingIntentCancelledListener(bubble -> { + if (bubble.getBubbleIntent() == null) { + return; + } + if (bubble.isIntentActive() || mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { + bubble.setPendingIntentCanceled(); + return; + } + mMainExecutor.execute(() -> removeBubble(bubble.getKey(), DISMISS_INVALID_INTENT)); + }); + + try { + windowManagerShellWrapper.addPinnedStackListener(new BubblesImeListener()); + } catch (RemoteException e) { + e.printStackTrace(); + } + mSurfaceSynchronizer = synchronizer; + + mWindowManager = windowManager; + mBarService = statusBarService == null + ? IStatusBarService.Stub.asInterface( + ServiceManager.getService(Context.STATUS_BAR_SERVICE)) + : statusBarService; + + mSavedBubbleKeysPerUser = new SparseSetArray<>(); + mCurrentUserId = ActivityManager.getCurrentUser(); + mBubbleData.setCurrentUserId(mCurrentUserId); + + mBubbleIconFactory = new BubbleIconFactory(context); + mTaskOrganizer = organizer; + + launcherApps.registerCallback(new LauncherApps.Callback() { + @Override + public void onPackageAdded(String s, UserHandle userHandle) {} + + @Override + public void onPackageChanged(String s, UserHandle userHandle) {} + + @Override + public void onPackageRemoved(String s, UserHandle userHandle) { + // Remove bubbles with this package name, since it has been uninstalled and attempts + // to open a bubble from an uninstalled app can cause issues. + mBubbleData.removeBubblesWithPackageName(s, DISMISS_PACKAGE_REMOVED); + } + + @Override + public void onPackagesAvailable(String[] strings, UserHandle userHandle, boolean b) {} + + @Override + public void onPackagesUnavailable(String[] packages, UserHandle userHandle, + boolean b) { + for (String packageName : packages) { + // Remove bubbles from unavailable apps. This can occur when the app is on + // external storage that has been removed. + mBubbleData.removeBubblesWithPackageName(packageName, DISMISS_PACKAGE_REMOVED); + } + } + + @Override + public void onShortcutsChanged(String packageName, List<ShortcutInfo> validShortcuts, + UserHandle user) { + super.onShortcutsChanged(packageName, validShortcuts, user); + + // Remove bubbles whose shortcuts aren't in the latest list of valid shortcuts. + mBubbleData.removeBubblesWithInvalidShortcuts( + packageName, validShortcuts, DISMISS_SHORTCUT_REMOVED); + } + }, mainHandler); + } + + @VisibleForTesting + public Bubbles getImpl() { + return mImpl; + } + + /** + * Hides the current input method, wherever it may be focused, via InputMethodManagerInternal. + */ + void hideCurrentInputMethod() { + try { + mBarService.hideCurrentInputMethodForBubbles(); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + private void openBubbleOverflow() { + ensureStackViewCreated(); + mBubbleData.setShowingOverflow(true); + mBubbleData.setSelectedBubble(mBubbleData.getOverflow()); + mBubbleData.setExpanded(true); + } + + /** Called when any taskbar state changes (e.g. visibility, position, sizes). */ + private void onTaskbarChanged(Bundle b) { + if (b == null) { + return; + } + boolean isVisible = b.getBoolean(EXTRA_TASKBAR_VISIBLE, false /* default */); + String position = b.getString(EXTRA_TASKBAR_POSITION, RIGHT_POSITION /* default */); + @BubblePositioner.TaskbarPosition int taskbarPosition = TASKBAR_POSITION_NONE; + switch (position) { + case LEFT_POSITION: + taskbarPosition = TASKBAR_POSITION_LEFT; + break; + case RIGHT_POSITION: + taskbarPosition = TASKBAR_POSITION_RIGHT; + break; + case BOTTOM_POSITION: + taskbarPosition = TASKBAR_POSITION_BOTTOM; + break; + } + int[] itemPosition = b.getIntArray(EXTRA_TASKBAR_BUBBLE_XY); + int iconSize = b.getInt(EXTRA_TASKBAR_ICON_SIZE); + int taskbarSize = b.getInt(EXTRA_TASKBAR_SIZE); + Log.w(TAG, "onTaskbarChanged:" + + " isVisible: " + isVisible + + " position: " + position + + " itemPosition: " + itemPosition[0] + "," + itemPosition[1] + + " iconSize: " + iconSize); + PointF point = new PointF(itemPosition[0], itemPosition[1]); + mBubblePositioner.setPinnedLocation(isVisible ? point : null); + mBubblePositioner.updateForTaskbar(iconSize, taskbarPosition, isVisible, taskbarSize); + if (mStackView != null) { + if (isVisible && b.getBoolean(EXTRA_TASKBAR_CREATED, false /* default */)) { + // If taskbar was created, add and remove the window so that bubbles display on top + removeFromWindowManagerMaybe(); + addToWindowManagerMaybe(); + } + mStackView.updateStackPosition(); + mBubbleIconFactory = new BubbleIconFactory(mContext); + mStackView.onDisplaySizeChanged(); + } + if (b.getBoolean(EXTRA_BUBBLE_OVERFLOW_OPENED, false)) { + openBubbleOverflow(); + } + } + + /** + * Called when the status bar has become visible or invisible (either permanently or + * temporarily). + */ + private void onStatusBarVisibilityChanged(boolean visible) { + if (mStackView != null) { + // Hide the stack temporarily if the status bar has been made invisible, and the stack + // is collapsed. An expanded stack should remain visible until collapsed. + mStackView.setTemporarilyInvisible(!visible && !isStackExpanded()); + } + } + + private void onZenStateChanged() { + for (Bubble b : mBubbleData.getBubbles()) { + b.setShowDot(b.showInShade()); + } + } + + private void onStatusBarStateChanged(boolean isShade) { + mIsStatusBarShade = isShade; + if (!mIsStatusBarShade) { + collapseStack(); + } + + if (mNotifEntryToExpandOnShadeUnlock != null) { + expandStackAndSelectBubble(mNotifEntryToExpandOnShadeUnlock); + mNotifEntryToExpandOnShadeUnlock = null; + } + + updateStack(); + } + + private void onUserChanged(int newUserId) { + saveBubbles(mCurrentUserId); + mBubbleData.dismissAll(DISMISS_USER_CHANGED); + restoreBubbles(newUserId); + mCurrentUserId = newUserId; + mBubbleData.setCurrentUserId(newUserId); + } + + /** + * Sets whether to perform inflation on the same thread as the caller. This method should only + * be used in tests, not in production. + */ + @VisibleForTesting + public void setInflateSynchronously(boolean inflateSynchronously) { + mInflateSynchronously = inflateSynchronously; + } + + /** Set a listener to be notified of when overflow view update. */ + public void setOverflowListener(BubbleData.Listener listener) { + mOverflowListener = listener; + } + + /** + * @return Bubbles for updating overflow. + */ + List<Bubble> getOverflowBubbles() { + return mBubbleData.getOverflowBubbles(); + } + + /** The task listener for events in bubble tasks. */ + public ShellTaskOrganizer getTaskOrganizer() { + return mTaskOrganizer; + } + + /** Contains information to help position things on the screen. */ + BubblePositioner getPositioner() { + return mBubblePositioner; + } + + Bubbles.SysuiProxy getSysuiProxy() { + return mSysuiProxy; + } + + /** + * BubbleStackView is lazily created by this method the first time a Bubble is added. This + * method initializes the stack view and adds it to the StatusBar just above the scrim. + */ + private void ensureStackViewCreated() { + if (mStackView == null) { + mStackView = new BubbleStackView( + mContext, this, mBubbleData, mSurfaceSynchronizer, mFloatingContentCoordinator, + mMainExecutor); + mStackView.onOrientationChanged(); + if (mExpandListener != null) { + mStackView.setExpandListener(mExpandListener); + } + mStackView.setUnbubbleConversationCallback(mSysuiProxy::onUnbubbleConversation); + } + + addToWindowManagerMaybe(); + } + + /** Adds the BubbleStackView to the WindowManager if it's not already there. */ + private void addToWindowManagerMaybe() { + // If the stack is null, or already added, don't add it. + if (mStackView == null || mAddedToWindowManager) { + return; + } + + mWmLayoutParams = new WindowManager.LayoutParams( + // Fill the screen so we can use translation animations to position the bubble + // stack. We'll use touchable regions to ignore touches that are not on the bubbles + // themselves. + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, + PixelFormat.TRANSLUCENT); + + mWmLayoutParams.setTrustedOverlay(); + mWmLayoutParams.setFitInsetsTypes(0); + mWmLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; + mWmLayoutParams.token = new Binder(); + mWmLayoutParams.setTitle("Bubbles!"); + mWmLayoutParams.packageName = mContext.getPackageName(); + mWmLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + + try { + mAddedToWindowManager = true; + mBubbleData.getOverflow().initialize(this); + mStackView.addView(mBubbleScrim); + mWindowManager.addView(mStackView, mWmLayoutParams); + // Position info is dependent on us being attached to a window + mBubblePositioner.update(mOrientation); + } catch (IllegalStateException e) { + // This means the stack has already been added. This shouldn't happen... + e.printStackTrace(); + } + } + + void onImeVisibilityChanged(boolean imeVisible) { + mImeVisible = imeVisible; + } + + /** Removes the BubbleStackView from the WindowManager if it's there. */ + private void removeFromWindowManagerMaybe() { + if (!mAddedToWindowManager) { + return; + } + + try { + mAddedToWindowManager = false; + if (mStackView != null) { + mWindowManager.removeView(mStackView); + mStackView.removeView(mBubbleScrim); + mBubbleData.getOverflow().cleanUpExpandedState(); + } else { + Log.w(TAG, "StackView added to WindowManager, but was null when removing!"); + } + } catch (IllegalArgumentException e) { + // This means the stack has already been removed - it shouldn't happen, but ignore if it + // does, since we wanted it removed anyway. + e.printStackTrace(); + } + } + + /** + * Called by the BubbleStackView and whenever all bubbles have animated out, and none have been + * added in the meantime. + */ + void onAllBubblesAnimatedOut() { + if (mStackView != null) { + mStackView.setVisibility(INVISIBLE); + removeFromWindowManagerMaybe(); + } + } + + /** + * Records the notification key for any active bubbles. These are used to restore active + * bubbles when the user returns to the foreground. + * + * @param userId the id of the user + */ + private void saveBubbles(@UserIdInt int userId) { + // First clear any existing keys that might be stored. + mSavedBubbleKeysPerUser.remove(userId); + // Add in all active bubbles for the current user. + for (Bubble bubble: mBubbleData.getBubbles()) { + mSavedBubbleKeysPerUser.add(userId, bubble.getKey()); + } + } + + /** + * Promotes existing notifications to Bubbles if they were previously bubbles. + * + * @param userId the id of the user + */ + private void restoreBubbles(@UserIdInt int userId) { + ArraySet<String> savedBubbleKeys = mSavedBubbleKeysPerUser.get(userId); + if (savedBubbleKeys == null) { + // There were no bubbles saved for this used. + return; + } + for (BubbleEntry e : mSysuiProxy.getShouldRestoredEntries(savedBubbleKeys)) { + if (canLaunchInActivityView(mContext, e)) { + updateBubble(e, true /* suppressFlyout */, false /* showInShade */); + } + } + // Finally, remove the entries for this user now that bubbles are restored. + mSavedBubbleKeysPerUser.remove(mCurrentUserId); + } + + private void updateForThemeChanges() { + if (mStackView != null) { + mStackView.onThemeChanged(); + } + mBubbleIconFactory = new BubbleIconFactory(mContext); + // Reload each bubble + for (Bubble b: mBubbleData.getBubbles()) { + b.inflate(null /* callback */, mContext, this, mStackView, mBubbleIconFactory, + false /* skipInflation */); + } + for (Bubble b: mBubbleData.getOverflowBubbles()) { + b.inflate(null /* callback */, mContext, this, mStackView, mBubbleIconFactory, + false /* skipInflation */); + } + } + + private void onConfigChanged(Configuration newConfig) { + if (mBubblePositioner != null) { + // This doesn't trigger any changes, always update it + mBubblePositioner.update(newConfig.orientation); + } + if (mStackView != null && newConfig != null) { + if (newConfig.orientation != mOrientation) { + mOrientation = newConfig.orientation; + mStackView.onOrientationChanged(); + } + if (newConfig.densityDpi != mDensityDpi) { + mDensityDpi = newConfig.densityDpi; + mBubbleIconFactory = new BubbleIconFactory(mContext); + mStackView.onDisplaySizeChanged(); + } + if (newConfig.fontScale != mFontScale) { + mFontScale = newConfig.fontScale; + mStackView.updateFontScale(mFontScale); + } + if (newConfig.getLayoutDirection() != mLayoutDirection) { + mLayoutDirection = newConfig.getLayoutDirection(); + mStackView.onLayoutDirectionChanged(mLayoutDirection); + } + } + } + + private void setBubbleScrim(View view, BiConsumer<Executor, Looper> callback) { + mBubbleScrim = view; + callback.accept(mMainExecutor, mMainExecutor.executeBlockingForResult(() -> { + return Looper.myLooper(); + }, Looper.class)); + } + + private void setSysuiProxy(Bubbles.SysuiProxy proxy) { + mSysuiProxy = proxy; + } + + @VisibleForTesting + public void setExpandListener(Bubbles.BubbleExpandListener listener) { + mExpandListener = ((isExpanding, key) -> { + if (listener != null) { + listener.onBubbleExpandChanged(isExpanding, key); + } + }); + if (mStackView != null) { + mStackView.setExpandListener(mExpandListener); + } + } + + /** + * Whether or not there are bubbles present, regardless of them being visible on the + * screen (e.g. if on AOD). + */ + @VisibleForTesting + public boolean hasBubbles() { + if (mStackView == null) { + return false; + } + return mBubbleData.hasBubbles() || mBubbleData.isShowingOverflow(); + } + + @VisibleForTesting + public boolean isStackExpanded() { + return mBubbleData.isExpanded(); + } + + @VisibleForTesting + public void collapseStack() { + mBubbleData.setExpanded(false /* expanded */); + } + + @VisibleForTesting + public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) { + boolean isSuppressedBubble = (mBubbleData.hasAnyBubbleWithKey(key) + && !mBubbleData.getAnyBubbleWithkey(key).showInShade()); + + boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey); + boolean isSummary = key.equals(mBubbleData.getSummaryKey(groupKey)); + return (isSummary && isSuppressedSummary) || isSuppressedBubble; + } + + private void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback, + Executor callbackExecutor) { + if (mBubbleData.isSummarySuppressed(groupKey)) { + mBubbleData.removeSuppressedSummary(groupKey); + if (callback != null) { + callbackExecutor.execute(() -> { + callback.accept(mBubbleData.getSummaryKey(groupKey)); + }); + } + } + } + + private boolean isBubbleExpanded(String key) { + return isStackExpanded() && mBubbleData != null && mBubbleData.getSelectedBubble() != null + && mBubbleData.getSelectedBubble().getKey().equals(key); + } + + /** Promote the provided bubble from the overflow view. */ + public void promoteBubbleFromOverflow(Bubble bubble) { + mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK); + bubble.setInflateSynchronously(mInflateSynchronously); + bubble.setShouldAutoExpand(true); + bubble.markAsAccessedAt(System.currentTimeMillis()); + setIsBubble(bubble, true /* isBubble */); + } + + @VisibleForTesting + public void expandStackAndSelectBubble(BubbleEntry entry) { + if (mIsStatusBarShade) { + mNotifEntryToExpandOnShadeUnlock = null; + + String key = entry.getKey(); + Bubble bubble = mBubbleData.getBubbleInStackWithKey(key); + if (bubble != null) { + mBubbleData.setSelectedBubble(bubble); + mBubbleData.setExpanded(true); + } else { + bubble = mBubbleData.getOverflowBubbleWithKey(key); + if (bubble != null) { + promoteBubbleFromOverflow(bubble); + } else if (entry.canBubble()) { + // It can bubble but it's not -- it got aged out of the overflow before it + // was dismissed or opened, make it a bubble again. + setIsBubble(entry, true /* isBubble */, true /* autoExpand */); + } + } + } else { + // Wait until we're unlocked to expand, so that the user can see the expand animation + // and also to work around bugs with expansion animation + shade unlock happening at the + // same time. + mNotifEntryToExpandOnShadeUnlock = entry; + } + } + + /** + * Adds or updates a bubble associated with the provided notification entry. + * + * @param notif the notification associated with this bubble. + */ + @VisibleForTesting + public void updateBubble(BubbleEntry notif) { + updateBubble(notif, false /* suppressFlyout */, true /* showInShade */); + } + + /** + * Fills the overflow bubbles by loading them from disk. + */ + void loadOverflowBubblesFromDisk() { + if (!mBubbleData.getOverflowBubbles().isEmpty() || mOverflowDataLoaded) { + // we don't need to load overflow bubbles from disk if it is already in memory + return; + } + mOverflowDataLoaded = true; + mDataRepository.loadBubbles((bubbles) -> { + bubbles.forEach(bubble -> { + if (mBubbleData.hasAnyBubbleWithKey(bubble.getKey())) { + // if the bubble is already active, there's no need to push it to overflow + return; + } + bubble.inflate( + (b) -> mBubbleData.overflowBubble(Bubbles.DISMISS_RELOAD_FROM_DISK, bubble), + mContext, this, mStackView, mBubbleIconFactory, true /* skipInflation */); + }); + return null; + }); + } + + /** + * Adds or updates a bubble associated with the provided notification entry. + * + * @param notif the notification associated with this bubble. + * @param suppressFlyout this bubble suppress flyout or not. + * @param showInShade this bubble show in shade or not. + */ + @VisibleForTesting + public void updateBubble(BubbleEntry notif, boolean suppressFlyout, boolean showInShade) { + // If this is an interruptive notif, mark that it's interrupted + mSysuiProxy.setNotificationInterruption(notif.getKey()); + if (!notif.getRanking().visuallyInterruptive() + && (notif.getBubbleMetadata() != null + && !notif.getBubbleMetadata().getAutoExpandBubble()) + && mBubbleData.hasOverflowBubbleWithKey(notif.getKey())) { + // Update the bubble but don't promote it out of overflow + Bubble b = mBubbleData.getOverflowBubbleWithKey(notif.getKey()); + b.setEntry(notif); + } else { + Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */); + inflateAndAdd(bubble, suppressFlyout, showInShade); + } + } + + void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) { + // Lazy init stack view when a bubble is created + ensureStackViewCreated(); + bubble.setInflateSynchronously(mInflateSynchronously); + bubble.inflate(b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade), + mContext, this, mStackView, mBubbleIconFactory, false /* skipInflation */); + } + + /** + * Removes the bubble with the given key. + * <p> + * Must be called from the main thread. + */ + @VisibleForTesting + @MainThread + public void removeBubble(String key, int reason) { + if (mBubbleData.hasAnyBubbleWithKey(key)) { + mBubbleData.dismissBubbleWithKey(key, reason); + } + } + + private void onEntryAdded(BubbleEntry entry) { + if (canLaunchInActivityView(mContext, entry)) { + updateBubble(entry); + } + } + + private void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) { + // shouldBubbleUp checks canBubble & for bubble metadata + boolean shouldBubble = shouldBubbleUp && canLaunchInActivityView(mContext, entry); + if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) { + // It was previously a bubble but no longer a bubble -- lets remove it + removeBubble(entry.getKey(), DISMISS_NO_LONGER_BUBBLE); + } else if (shouldBubble && entry.isBubble()) { + updateBubble(entry); + } + } + + private void onEntryRemoved(BubbleEntry entry) { + if (isSummaryOfBubbles(entry)) { + final String groupKey = entry.getStatusBarNotification().getGroupKey(); + mBubbleData.removeSuppressedSummary(groupKey); + + // Remove any associated bubble children with the summary + final List<Bubble> bubbleChildren = getBubblesInGroup(groupKey); + for (int i = 0; i < bubbleChildren.size(); i++) { + removeBubble(bubbleChildren.get(i).getKey(), DISMISS_GROUP_CANCELLED); + } + } else { + removeBubble(entry.getKey(), DISMISS_NOTIF_CANCEL); + } + } + + private void onRankingUpdated(RankingMap rankingMap) { + if (mTmpRanking == null) { + mTmpRanking = new NotificationListenerService.Ranking(); + } + String[] orderedKeys = rankingMap.getOrderedKeys(); + for (int i = 0; i < orderedKeys.length; i++) { + String key = orderedKeys[i]; + BubbleEntry entry = mSysuiProxy.getPendingOrActiveEntry(key); + rankingMap.getRanking(key, mTmpRanking); + boolean isActiveBubble = mBubbleData.hasAnyBubbleWithKey(key); + if (isActiveBubble && !mTmpRanking.canBubble()) { + // If this entry is no longer allowed to bubble, dismiss with the BLOCKED reason. + // This means that the app or channel's ability to bubble has been revoked. + mBubbleData.dismissBubbleWithKey(key, DISMISS_BLOCKED); + } else if (isActiveBubble && !mSysuiProxy.shouldBubbleUp(key)) { + // If this entry is allowed to bubble, but cannot currently bubble up, dismiss it. + // This happens when DND is enabled and configured to hide bubbles. Dismissing with + // the reason DISMISS_NO_BUBBLE_UP will retain the underlying notification, so that + // the bubble will be re-created if shouldBubbleUp returns true. + mBubbleData.dismissBubbleWithKey(key, DISMISS_NO_BUBBLE_UP); + } else if (entry != null && mTmpRanking.isBubble() && !isActiveBubble) { + entry.setFlagBubble(true); + onEntryUpdated(entry, true /* shouldBubbleUp */); + } + } + } + + /** + * Retrieves any bubbles that are part of the notification group represented by the provided + * group key. + */ + private ArrayList<Bubble> getBubblesInGroup(@Nullable String groupKey) { + ArrayList<Bubble> bubbleChildren = new ArrayList<>(); + if (groupKey == null) { + return bubbleChildren; + } + for (Bubble bubble : mBubbleData.getActiveBubbles()) { + // TODO(178620678): Prevent calling into SysUI since this can be a part of a blocking + // call from SysUI to Shell + final BubbleEntry entry = mSysuiProxy.getPendingOrActiveEntry(bubble.getKey()); + if (entry != null && groupKey.equals(entry.getStatusBarNotification().getGroupKey())) { + bubbleChildren.add(bubble); + } + } + return bubbleChildren; + } + + private void setIsBubble(@NonNull final BubbleEntry entry, final boolean isBubble, + final boolean autoExpand) { + Objects.requireNonNull(entry); + entry.setFlagBubble(isBubble); + try { + int flags = 0; + if (autoExpand) { + flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; + flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE; + } + mBarService.onNotificationBubbleChanged(entry.getKey(), isBubble, flags); + } catch (RemoteException e) { + // Bad things have happened + } + } + + private void setIsBubble(@NonNull final Bubble b, final boolean isBubble) { + Objects.requireNonNull(b); + b.setIsBubble(isBubble); + final BubbleEntry entry = mSysuiProxy.getPendingOrActiveEntry(b.getKey()); + if (entry != null) { + // Updating the entry to be a bubble will trigger our normal update flow + setIsBubble(entry, isBubble, b.shouldAutoExpand()); + } else if (isBubble) { + // If bubble doesn't exist, it's a persisted bubble so we need to add it to the + // stack ourselves + Bubble bubble = mBubbleData.getOrCreateBubble(null, b /* persistedBubble */); + inflateAndAdd(bubble, bubble.shouldAutoExpand() /* suppressFlyout */, + !bubble.shouldAutoExpand() /* showInShade */); + } + } + + @SuppressWarnings("FieldCanBeLocal") + private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() { + + @Override + public void applyUpdate(BubbleData.Update update) { + ensureStackViewCreated(); + + // Lazy load overflow bubbles from disk + loadOverflowBubblesFromDisk(); + + mStackView.updateOverflowButtonDot(); + + // Update bubbles in overflow. + if (mOverflowListener != null) { + mOverflowListener.applyUpdate(update); + } + + // Collapsing? Do this first before remaining steps. + if (update.expandedChanged && !update.expanded) { + mStackView.setExpanded(false); + mSysuiProxy.requestNotificationShadeTopUi(false, TAG); + } + + // Do removals, if any. + ArrayList<Pair<Bubble, Integer>> removedBubbles = + new ArrayList<>(update.removedBubbles); + ArrayList<Bubble> bubblesToBeRemovedFromRepository = new ArrayList<>(); + for (Pair<Bubble, Integer> removed : removedBubbles) { + final Bubble bubble = removed.first; + @Bubbles.DismissReason final int reason = removed.second; + + if (mStackView != null) { + mStackView.removeBubble(bubble); + } + + // Leave the notification in place if we're dismissing due to user switching, or + // because DND is suppressing the bubble. In both of those cases, we need to be able + // to restore the bubble from the notification later. + if (reason == DISMISS_USER_CHANGED || reason == DISMISS_NO_BUBBLE_UP) { + continue; + } + if (reason == DISMISS_NOTIF_CANCEL) { + bubblesToBeRemovedFromRepository.add(bubble); + } + if (!mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { + if (!mBubbleData.hasOverflowBubbleWithKey(bubble.getKey()) + && (!bubble.showInShade() + || reason == DISMISS_NOTIF_CANCEL + || reason == DISMISS_GROUP_CANCELLED)) { + // The bubble is now gone & the notification is hidden from the shade, so + // time to actually remove it + mSysuiProxy.notifyRemoveNotification(bubble.getKey(), REASON_CANCEL); + } else { + if (bubble.isBubble()) { + setIsBubble(bubble, false /* isBubble */); + } + mSysuiProxy.updateNotificationBubbleButton(bubble.getKey()); + } + + } + final BubbleEntry entry = mSysuiProxy.getPendingOrActiveEntry(bubble.getKey()); + if (entry != null) { + final String groupKey = entry.getStatusBarNotification().getGroupKey(); + if (getBubblesInGroup(groupKey).isEmpty()) { + // Time to potentially remove the summary + mSysuiProxy.notifyMaybeCancelSummary(bubble.getKey()); + } + } + } + mDataRepository.removeBubbles(mCurrentUserId, bubblesToBeRemovedFromRepository); + + if (update.addedBubble != null && mStackView != null) { + mDataRepository.addBubble(mCurrentUserId, update.addedBubble); + mStackView.addBubble(update.addedBubble); + } + + if (update.updatedBubble != null && mStackView != null) { + mStackView.updateBubble(update.updatedBubble); + } + + // At this point, the correct bubbles are inflated in the stack. + // Make sure the order in bubble data is reflected in bubble row. + if (update.orderChanged && mStackView != null) { + mDataRepository.addBubbles(mCurrentUserId, update.bubbles); + mStackView.updateBubbleOrder(update.bubbles); + } + + if (update.selectionChanged && mStackView != null) { + mStackView.setSelectedBubble(update.selectedBubble); + if (update.selectedBubble != null) { + mSysuiProxy.updateNotificationSuppression(update.selectedBubble.getKey()); + } + } + + // Expanding? Apply this last. + if (update.expandedChanged && update.expanded) { + if (mStackView != null) { + mStackView.setExpanded(true); + mSysuiProxy.requestNotificationShadeTopUi(true, TAG); + } + } + + mSysuiProxy.notifyInvalidateNotifications("BubbleData.Listener.applyUpdate"); + updateStack(); + } + }; + + private boolean handleDismissalInterception(BubbleEntry entry, + @Nullable List<BubbleEntry> children, IntConsumer removeCallback) { + if (isSummaryOfBubbles(entry)) { + handleSummaryDismissalInterception(entry, children, removeCallback); + } else { + Bubble bubble = mBubbleData.getBubbleInStackWithKey(entry.getKey()); + if (bubble == null || !entry.isBubble()) { + bubble = mBubbleData.getOverflowBubbleWithKey(entry.getKey()); + } + if (bubble == null) { + return false; + } + bubble.setSuppressNotification(true); + bubble.setShowDot(false /* show */); + } + // Update the shade + mSysuiProxy.notifyInvalidateNotifications("BubbleController.handleDismissalInterception"); + return true; + } + + private boolean isSummaryOfBubbles(BubbleEntry entry) { + String groupKey = entry.getStatusBarNotification().getGroupKey(); + ArrayList<Bubble> bubbleChildren = getBubblesInGroup(groupKey); + boolean isSuppressedSummary = (mBubbleData.isSummarySuppressed(groupKey) + && mBubbleData.getSummaryKey(groupKey).equals(entry.getKey())); + boolean isSummary = entry.getStatusBarNotification().getNotification().isGroupSummary(); + return (isSuppressedSummary || isSummary) && !bubbleChildren.isEmpty(); + } + + private void handleSummaryDismissalInterception( + BubbleEntry summary, @Nullable List<BubbleEntry> children, IntConsumer removeCallback) { + if (children != null) { + for (int i = 0; i < children.size(); i++) { + BubbleEntry child = children.get(i); + if (mBubbleData.hasAnyBubbleWithKey(child.getKey())) { + // Suppress the bubbled child + // As far as group manager is concerned, once a child is no longer shown + // in the shade, it is essentially removed. + Bubble bubbleChild = mBubbleData.getAnyBubbleWithkey(child.getKey()); + if (bubbleChild != null) { + mSysuiProxy.removeNotificationEntry(bubbleChild.getKey()); + bubbleChild.setSuppressNotification(true); + bubbleChild.setShowDot(false /* show */); + } + } else { + // non-bubbled children can be removed + removeCallback.accept(i); + } + } + } + + // And since all children are removed, remove the summary. + removeCallback.accept(-1); + + // TODO: (b/145659174) remove references to mSuppressedGroupKeys once fully migrated + mBubbleData.addSummaryToSuppress(summary.getStatusBarNotification().getGroupKey(), + summary.getKey()); + } + + /** + * Updates the visibility of the bubbles based on current state. + * Does not un-bubble, just hides or un-hides. + * Updates stack description for TalkBack focus. + */ + public void updateStack() { + if (mStackView == null) { + return; + } + + if (!mIsStatusBarShade) { + // Bubbles don't appear over the locked shade. + mStackView.setVisibility(INVISIBLE); + } else if (hasBubbles()) { + // If we're unlocked, show the stack if we have bubbles. If we don't have bubbles, the + // stack will be set to INVISIBLE in onAllBubblesAnimatedOut after the bubbles animate + // out. + mStackView.setVisibility(VISIBLE); + } + + mStackView.updateContentDescription(); + } + + /** + * The task id of the expanded view, if the stack is expanded and not occluded by the + * status bar, otherwise returns {@link ActivityTaskManager#INVALID_TASK_ID}. + */ + private int getExpandedTaskId() { + if (mStackView == null) { + return INVALID_TASK_ID; + } + final BubbleViewProvider expandedViewProvider = mStackView.getExpandedBubble(); + if (expandedViewProvider != null && isStackExpanded() + && !mStackView.isExpansionAnimating() + && !mSysuiProxy.isNotificationShadeExpand()) { + return expandedViewProvider.getTaskId(); + } + return INVALID_TASK_ID; + } + + @VisibleForTesting + public BubbleStackView getStackView() { + return mStackView; + } + + /** + * Description of current bubble state. + */ + private void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("BubbleController state:"); + mBubbleData.dump(fd, pw, args); + pw.println(); + if (mStackView != null) { + mStackView.dump(fd, pw, args); + } + pw.println(); + } + + /** + * Whether an intent is properly configured to display in an {@link android.app.ActivityView}. + * + * Keep checks in sync with NotificationManagerService#canLaunchInActivityView. Typically + * that should filter out any invalid bubbles, but should protect SysUI side just in case. + * + * @param context the context to use. + * @param entry the entry to bubble. + */ + static boolean canLaunchInActivityView(Context context, BubbleEntry entry) { + PendingIntent intent = entry.getBubbleMetadata() != null + ? entry.getBubbleMetadata().getIntent() + : null; + if (entry.getBubbleMetadata() != null + && entry.getBubbleMetadata().getShortcutId() != null) { + return true; + } + if (intent == null) { + Log.w(TAG, "Unable to create bubble -- no intent: " + entry.getKey()); + return false; + } + PackageManager packageManager = getPackageManagerForUser( + context, entry.getStatusBarNotification().getUser().getIdentifier()); + ActivityInfo info = + intent.getIntent().resolveActivityInfo(packageManager, 0); + if (info == null) { + Log.w(TAG, "Unable to send as bubble, " + + entry.getKey() + " couldn't find activity info for intent: " + + intent); + return false; + } + if (!ActivityInfo.isResizeableMode(info.resizeMode)) { + Log.w(TAG, "Unable to send as bubble, " + + entry.getKey() + " activity is not resizable for intent: " + + intent); + return false; + } + return true; + } + + static PackageManager getPackageManagerForUser(Context context, int userId) { + Context contextForUser = context; + // UserHandle defines special userId as negative values, e.g. USER_ALL + if (userId >= 0) { + try { + // Create a context for the correct user so if a package isn't installed + // for user 0 we can still load information about the package. + contextForUser = + context.createPackageContextAsUser(context.getPackageName(), + Context.CONTEXT_RESTRICTED, + new UserHandle(userId)); + } catch (PackageManager.NameNotFoundException e) { + // Shouldn't fail to find the package name for system ui. + } + } + return contextForUser.getPackageManager(); + } + + /** PinnedStackListener that dispatches IME visibility updates to the stack. */ + //TODO(b/170442945): Better way to do this / insets listener? + private class BubblesImeListener extends PinnedStackListenerForwarder.PinnedStackListener { + @Override + public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { + if (mStackView != null) { + mStackView.onImeVisibilityChanged(imeVisible, imeHeight); + } + } + } + + private class BubblesImpl implements Bubbles { + @Override + public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) { + return mMainExecutor.executeBlockingForResult(() -> { + return BubbleController.this.isBubbleNotificationSuppressedFromShade(key, groupKey); + }, Boolean.class); + } + + @Override + public boolean isBubbleExpanded(String key) { + return mMainExecutor.executeBlockingForResult(() -> { + return BubbleController.this.isBubbleExpanded(key); + }, Boolean.class); + } + + @Override + public boolean isStackExpanded() { + return mMainExecutor.executeBlockingForResult(() -> { + return BubbleController.this.isStackExpanded(); + }, Boolean.class); + } + + @Override + public void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback, + Executor callbackExecutor) { + mMainExecutor.execute(() -> { + BubbleController.this.removeSuppressedSummaryIfNecessary(groupKey, callback, + callbackExecutor); + }); + } + + @Override + public void collapseStack() { + mMainExecutor.execute(() -> { + BubbleController.this.collapseStack(); + }); + } + + @Override + public void updateForThemeChanges() { + mMainExecutor.execute(() -> { + BubbleController.this.updateForThemeChanges(); + }); + } + + @Override + public void expandStackAndSelectBubble(BubbleEntry entry) { + mMainExecutor.execute(() -> { + BubbleController.this.expandStackAndSelectBubble(entry); + }); + } + + @Override + public void onTaskbarChanged(Bundle b) { + mMainExecutor.execute(() -> { + BubbleController.this.onTaskbarChanged(b); + }); + } + + @Override + public void openBubbleOverflow() { + mMainExecutor.execute(() -> { + BubbleController.this.openBubbleOverflow(); + }); + } + + @Override + public boolean handleDismissalInterception(BubbleEntry entry, + @Nullable List<BubbleEntry> children, IntConsumer removeCallback) { + return mMainExecutor.executeBlockingForResult(() -> { + return BubbleController.this.handleDismissalInterception(entry, children, + removeCallback); + }, Boolean.class); + } + + @Override + public void setSysuiProxy(SysuiProxy proxy) { + mMainExecutor.execute(() -> { + BubbleController.this.setSysuiProxy(proxy); + }); + } + + @Override + public void setBubbleScrim(View view, BiConsumer<Executor, Looper> callback) { + mMainExecutor.execute(() -> { + BubbleController.this.setBubbleScrim(view, callback); + }); + } + + @Override + public void setExpandListener(BubbleExpandListener listener) { + mMainExecutor.execute(() -> { + BubbleController.this.setExpandListener(listener); + }); + } + + @Override + public void onEntryAdded(BubbleEntry entry) { + mMainExecutor.execute(() -> { + BubbleController.this.onEntryAdded(entry); + }); + } + + @Override + public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) { + mMainExecutor.execute(() -> { + BubbleController.this.onEntryUpdated(entry, shouldBubbleUp); + }); + } + + @Override + public void onEntryRemoved(BubbleEntry entry) { + mMainExecutor.execute(() -> { + BubbleController.this.onEntryRemoved(entry); + }); + } + + @Override + public void onRankingUpdated(RankingMap rankingMap) { + mMainExecutor.execute(() -> { + BubbleController.this.onRankingUpdated(rankingMap); + }); + } + + @Override + public void onStatusBarVisibilityChanged(boolean visible) { + mMainExecutor.execute(() -> { + BubbleController.this.onStatusBarVisibilityChanged(visible); + }); + } + + @Override + public void onZenStateChanged() { + mMainExecutor.execute(() -> { + BubbleController.this.onZenStateChanged(); + }); + } + + @Override + public void onStatusBarStateChanged(boolean isShade) { + mMainExecutor.execute(() -> { + BubbleController.this.onStatusBarStateChanged(isShade); + }); + } + + @Override + public void onUserChanged(int newUserId) { + mMainExecutor.execute(() -> { + BubbleController.this.onUserChanged(newUserId); + }); + } + + @Override + public void onConfigChanged(Configuration newConfig) { + mMainExecutor.execute(() -> { + BubbleController.this.onConfigChanged(newConfig); + }); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + try { + mMainExecutor.executeBlocking(() -> { + BubbleController.this.dump(fd, pw, args); + }); + } catch (InterruptedException e) { + Slog.e(TAG, "Failed to dump BubbleController in 2s"); + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java new file mode 100644 index 000000000000..53b75373a647 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -0,0 +1,863 @@ +/* + * 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.bubbles; + +import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; +import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_DATA; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; + +import android.annotation.NonNull; +import android.app.PendingIntent; +import android.content.Context; +import android.content.pm.ShortcutInfo; +import android.util.Log; +import android.util.Pair; +import android.view.View; + +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FrameworkStatsLog; +import com.android.wm.shell.R; +import com.android.wm.shell.bubbles.Bubbles.DismissReason; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Keeps track of active bubbles. + */ +public class BubbleData { + + private BubbleLogger mLogger; + + private int mCurrentUserId; + + private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleData" : TAG_BUBBLES; + + private static final Comparator<Bubble> BUBBLES_BY_SORT_KEY_DESCENDING = + Comparator.comparing(BubbleData::sortKey).reversed(); + + /** Contains information about changes that have been made to the state of bubbles. */ + static final class Update { + boolean expandedChanged; + boolean selectionChanged; + boolean orderChanged; + boolean expanded; + @Nullable BubbleViewProvider selectedBubble; + @Nullable Bubble addedBubble; + @Nullable Bubble updatedBubble; + @Nullable Bubble addedOverflowBubble; + @Nullable Bubble removedOverflowBubble; + // Pair with Bubble and @DismissReason Integer + final List<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>(); + + // A read-only view of the bubbles list, changes there will be reflected here. + final List<Bubble> bubbles; + final List<Bubble> overflowBubbles; + + private Update(List<Bubble> row, List<Bubble> overflow) { + bubbles = Collections.unmodifiableList(row); + overflowBubbles = Collections.unmodifiableList(overflow); + } + + boolean anythingChanged() { + return expandedChanged + || selectionChanged + || addedBubble != null + || updatedBubble != null + || !removedBubbles.isEmpty() + || addedOverflowBubble != null + || removedOverflowBubble != null + || orderChanged; + } + + void bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason) { + removedBubbles.add(new Pair<>(bubbleToRemove, reason)); + } + } + + /** + * This interface reports changes to the state and appearance of bubbles which should be applied + * as necessary to the UI. + */ + interface Listener { + /** Reports changes have have occurred as a result of the most recent operation. */ + void applyUpdate(Update update); + } + + interface TimeSource { + long currentTimeMillis(); + } + + private final Context mContext; + private final BubblePositioner mPositioner; + private final Executor mMainExecutor; + /** Bubbles that are actively in the stack. */ + private final List<Bubble> mBubbles; + /** Bubbles that aged out to overflow. */ + private final List<Bubble> mOverflowBubbles; + /** Bubbles that are being loaded but haven't been added to the stack just yet. */ + private final HashMap<String, Bubble> mPendingBubbles; + private BubbleViewProvider mSelectedBubble; + private final BubbleOverflow mOverflow; + private boolean mShowingOverflow; + private boolean mExpanded; + private final int mMaxBubbles; + private int mMaxOverflowBubbles; + + // State tracked during an operation -- keeps track of what listener events to dispatch. + private Update mStateChange; + + private TimeSource mTimeSource = System::currentTimeMillis; + + @Nullable + private Listener mListener; + + @Nullable + private Bubbles.NotificationSuppressionChangedListener mSuppressionListener; + private Bubbles.PendingIntentCanceledListener mCancelledListener; + + /** + * We track groups with summaries that aren't visibly displayed but still kept around because + * the bubble(s) associated with the summary still exist. + * + * The summary must be kept around so that developers can cancel it (and hence the bubbles + * associated with it). This list is used to check if the summary should be hidden from the + * shade. + * + * Key: group key of the notification + * Value: key of the notification + */ + private HashMap<String, String> mSuppressedGroupKeys = new HashMap<>(); + + public BubbleData(Context context, BubbleLogger bubbleLogger, BubblePositioner positioner, + Executor mainExecutor) { + mContext = context; + mLogger = bubbleLogger; + mPositioner = positioner; + mMainExecutor = mainExecutor; + mOverflow = new BubbleOverflow(context, positioner); + mBubbles = new ArrayList<>(); + mOverflowBubbles = new ArrayList<>(); + mPendingBubbles = new HashMap<>(); + mStateChange = new Update(mBubbles, mOverflowBubbles); + mMaxBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_rendered); + mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow); + } + + public void setSuppressionChangedListener( + Bubbles.NotificationSuppressionChangedListener listener) { + mSuppressionListener = listener; + } + + public void setPendingIntentCancelledListener( + Bubbles.PendingIntentCanceledListener listener) { + mCancelledListener = listener; + } + + public boolean hasBubbles() { + return !mBubbles.isEmpty(); + } + + public boolean hasOverflowBubbles() { + return !mOverflowBubbles.isEmpty(); + } + + public boolean isExpanded() { + return mExpanded; + } + + public boolean hasAnyBubbleWithKey(String key) { + return hasBubbleInStackWithKey(key) || hasOverflowBubbleWithKey(key); + } + + public boolean hasBubbleInStackWithKey(String key) { + return getBubbleInStackWithKey(key) != null; + } + + public boolean hasOverflowBubbleWithKey(String key) { + return getOverflowBubbleWithKey(key) != null; + } + + @Nullable + public BubbleViewProvider getSelectedBubble() { + return mSelectedBubble; + } + + public BubbleOverflow getOverflow() { + return mOverflow; + } + + /** Return a read-only current active bubble lists. */ + public List<Bubble> getActiveBubbles() { + return Collections.unmodifiableList(mBubbles); + } + + public void setExpanded(boolean expanded) { + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "setExpanded: " + expanded); + } + setExpandedInternal(expanded); + dispatchPendingChanges(); + } + + public void setSelectedBubble(BubbleViewProvider bubble) { + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "setSelectedBubble: " + bubble); + } + setSelectedBubbleInternal(bubble); + dispatchPendingChanges(); + } + + void setShowingOverflow(boolean showingOverflow) { + mShowingOverflow = showingOverflow; + } + + boolean isShowingOverflow() { + return mShowingOverflow && (isExpanded() || mPositioner.showingInTaskbar()); + } + + /** + * Constructs a new bubble or returns an existing one. Does not add new bubbles to + * bubble data, must go through {@link #notificationEntryUpdated(Bubble, boolean, boolean)} + * for that. + * + * @param entry The notification entry to use, only null if it's a bubble being promoted from + * the overflow that was persisted over reboot. + * @param persistedBubble The bubble to use, only non-null if it's a bubble being promoted from + * the overflow that was persisted over reboot. + */ + public Bubble getOrCreateBubble(BubbleEntry entry, Bubble persistedBubble) { + String key = persistedBubble != null ? persistedBubble.getKey() : entry.getKey(); + Bubble bubbleToReturn = getBubbleInStackWithKey(key); + + if (bubbleToReturn == null) { + bubbleToReturn = getOverflowBubbleWithKey(key); + if (bubbleToReturn != null) { + // Promoting from overflow + mOverflowBubbles.remove(bubbleToReturn); + } else if (mPendingBubbles.containsKey(key)) { + // Update while it was pending + bubbleToReturn = mPendingBubbles.get(key); + } else if (entry != null) { + // New bubble + bubbleToReturn = new Bubble(entry, mSuppressionListener, mCancelledListener, + mMainExecutor); + } else { + // Persisted bubble being promoted + bubbleToReturn = persistedBubble; + } + } + + if (entry != null) { + bubbleToReturn.setEntry(entry); + } + mPendingBubbles.put(key, bubbleToReturn); + return bubbleToReturn; + } + + /** + * When this method is called it is expected that all info in the bubble has completed loading. + * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, BubbleController, BubbleStackView, + * BubbleIconFactory, boolean) + */ + void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) { + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "notificationEntryUpdated: " + bubble); + } + mPendingBubbles.remove(bubble.getKey()); // No longer pending once we're here + Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey()); + suppressFlyout |= !bubble.isVisuallyInterruptive(); + + if (prevBubble == null) { + // Create a new bubble + bubble.setSuppressFlyout(suppressFlyout); + doAdd(bubble); + trim(); + } else { + // Updates an existing bubble + bubble.setSuppressFlyout(suppressFlyout); + // If there is no flyout, we probably shouldn't show the bubble at the top + doUpdate(bubble, !suppressFlyout /* reorder */); + } + + if (bubble.shouldAutoExpand()) { + bubble.setShouldAutoExpand(false); + setSelectedBubbleInternal(bubble); + if (!mExpanded) { + setExpandedInternal(true); + } + } + + boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble; + boolean suppress = isBubbleExpandedAndSelected || !showInShade || !bubble.showInShade(); + bubble.setSuppressNotification(suppress); + bubble.setShowDot(!isBubbleExpandedAndSelected /* show */); + + dispatchPendingChanges(); + } + + /** + * Dismisses the bubble with the matching key, if it exists. + */ + public void dismissBubbleWithKey(String key, @DismissReason int reason) { + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "notificationEntryRemoved: key=" + key + " reason=" + reason); + } + doRemove(key, reason); + dispatchPendingChanges(); + } + + /** + * Adds a group key indicating that the summary for this group should be suppressed. + * + * @param groupKey the group key of the group whose summary should be suppressed. + * @param notifKey the notification entry key of that summary. + */ + void addSummaryToSuppress(String groupKey, String notifKey) { + mSuppressedGroupKeys.put(groupKey, notifKey); + } + + /** + * Retrieves the notif entry key of the summary associated with the provided group key. + * + * @param groupKey the group to look up + * @return the key for the notification that is the summary of this group. + */ + String getSummaryKey(String groupKey) { + return mSuppressedGroupKeys.get(groupKey); + } + + /** + * Removes a group key indicating that summary for this group should no longer be suppressed. + */ + void removeSuppressedSummary(String groupKey) { + mSuppressedGroupKeys.remove(groupKey); + } + + /** + * Whether the summary for the provided group key is suppressed. + */ + @VisibleForTesting + public boolean isSummarySuppressed(String groupKey) { + return mSuppressedGroupKeys.containsKey(groupKey); + } + + /** + * Removes bubbles from the given package whose shortcut are not in the provided list of valid + * shortcuts. + */ + public void removeBubblesWithInvalidShortcuts( + String packageName, List<ShortcutInfo> validShortcuts, int reason) { + + final Set<String> validShortcutIds = new HashSet<String>(); + for (ShortcutInfo info : validShortcuts) { + validShortcutIds.add(info.getId()); + } + + final Predicate<Bubble> invalidBubblesFromPackage = bubble -> { + final boolean bubbleIsFromPackage = packageName.equals(bubble.getPackageName()); + final boolean isShortcutBubble = bubble.hasMetadataShortcutId(); + if (!bubbleIsFromPackage || !isShortcutBubble) { + return false; + } + final boolean hasShortcutIdAndValidShortcut = + bubble.hasMetadataShortcutId() + && bubble.getShortcutInfo() != null + && bubble.getShortcutInfo().isEnabled() + && validShortcutIds.contains(bubble.getShortcutInfo().getId()); + return bubbleIsFromPackage && !hasShortcutIdAndValidShortcut; + }; + + final Consumer<Bubble> removeBubble = bubble -> + dismissBubbleWithKey(bubble.getKey(), reason); + + performActionOnBubblesMatching(getBubbles(), invalidBubblesFromPackage, removeBubble); + performActionOnBubblesMatching( + getOverflowBubbles(), invalidBubblesFromPackage, removeBubble); + } + + /** Dismisses all bubbles from the given package. */ + public void removeBubblesWithPackageName(String packageName, int reason) { + final Predicate<Bubble> bubbleMatchesPackage = bubble -> + bubble.getPackageName().equals(packageName); + + final Consumer<Bubble> removeBubble = bubble -> + dismissBubbleWithKey(bubble.getKey(), reason); + + performActionOnBubblesMatching(getBubbles(), bubbleMatchesPackage, removeBubble); + performActionOnBubblesMatching(getOverflowBubbles(), bubbleMatchesPackage, removeBubble); + } + + private void doAdd(Bubble bubble) { + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "doAdd: " + bubble); + } + mBubbles.add(0, bubble); + mStateChange.addedBubble = bubble; + // Adding the first bubble doesn't change the order + mStateChange.orderChanged = mBubbles.size() > 1; + if (!isExpanded()) { + setSelectedBubbleInternal(mBubbles.get(0)); + } + } + + private void trim() { + if (mBubbles.size() > mMaxBubbles) { + mBubbles.stream() + // sort oldest first (ascending lastActivity) + .sorted(Comparator.comparingLong(Bubble::getLastActivity)) + // skip the selected bubble + .filter((b) -> !b.equals(mSelectedBubble)) + .findFirst() + .ifPresent((b) -> doRemove(b.getKey(), Bubbles.DISMISS_AGED)); + } + } + + private void doUpdate(Bubble bubble, boolean reorder) { + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "doUpdate: " + bubble); + } + mStateChange.updatedBubble = bubble; + if (!isExpanded() && reorder) { + int prevPos = mBubbles.indexOf(bubble); + mBubbles.remove(bubble); + mBubbles.add(0, bubble); + mStateChange.orderChanged = prevPos != 0; + setSelectedBubbleInternal(mBubbles.get(0)); + } + } + + /** Runs the given action on Bubbles that match the given predicate. */ + private void performActionOnBubblesMatching( + List<Bubble> bubbles, Predicate<Bubble> predicate, Consumer<Bubble> action) { + final List<Bubble> matchingBubbles = new ArrayList<>(); + for (Bubble bubble : bubbles) { + if (predicate.test(bubble)) { + matchingBubbles.add(bubble); + } + } + + for (Bubble matchingBubble : matchingBubbles) { + action.accept(matchingBubble); + } + } + + private void doRemove(String key, @DismissReason int reason) { + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "doRemove: " + key); + } + // If it was pending remove it + if (mPendingBubbles.containsKey(key)) { + mPendingBubbles.remove(key); + } + int indexToRemove = indexForKey(key); + if (indexToRemove == -1) { + if (hasOverflowBubbleWithKey(key) + && (reason == Bubbles.DISMISS_NOTIF_CANCEL + || reason == Bubbles.DISMISS_GROUP_CANCELLED + || reason == Bubbles.DISMISS_NO_LONGER_BUBBLE + || reason == Bubbles.DISMISS_BLOCKED + || reason == Bubbles.DISMISS_SHORTCUT_REMOVED + || reason == Bubbles.DISMISS_PACKAGE_REMOVED)) { + + Bubble b = getOverflowBubbleWithKey(key); + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "Cancel overflow bubble: " + b); + } + if (b != null) { + b.stopInflation(); + } + mLogger.logOverflowRemove(b, reason); + mOverflowBubbles.remove(b); + mStateChange.bubbleRemoved(b, reason); + mStateChange.removedOverflowBubble = b; + } + return; + } + Bubble bubbleToRemove = mBubbles.get(indexToRemove); + bubbleToRemove.stopInflation(); + if (mBubbles.size() == 1) { + if (hasOverflowBubbles() && (mPositioner.showingInTaskbar() || isExpanded())) { + // No more active bubbles but we have stuff in the overflow -- select that view + // if we're already expanded or always showing. + setShowingOverflow(true); + setSelectedBubbleInternal(mOverflow); + } else { + setExpandedInternal(false); + // Don't use setSelectedBubbleInternal because we don't want to trigger an + // applyUpdate + mSelectedBubble = null; + } + } + if (indexToRemove < mBubbles.size() - 1) { + // Removing anything but the last bubble means positions will change. + mStateChange.orderChanged = true; + } + mBubbles.remove(indexToRemove); + mStateChange.bubbleRemoved(bubbleToRemove, reason); + if (!isExpanded()) { + mStateChange.orderChanged |= repackAll(); + } + + overflowBubble(reason, bubbleToRemove); + + // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null. + if (Objects.equals(mSelectedBubble, bubbleToRemove)) { + // Move selection to the new bubble at the same position. + int newIndex = Math.min(indexToRemove, mBubbles.size() - 1); + BubbleViewProvider newSelected = mBubbles.get(newIndex); + setSelectedBubbleInternal(newSelected); + } + maybeSendDeleteIntent(reason, bubbleToRemove); + } + + void overflowBubble(@DismissReason int reason, Bubble bubble) { + if (bubble.getPendingIntentCanceled() + || !(reason == Bubbles.DISMISS_AGED + || reason == Bubbles.DISMISS_USER_GESTURE + || reason == Bubbles.DISMISS_RELOAD_FROM_DISK)) { + return; + } + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "Overflowing: " + bubble); + } + mLogger.logOverflowAdd(bubble, reason); + mOverflowBubbles.add(0, bubble); + mStateChange.addedOverflowBubble = bubble; + bubble.stopInflation(); + if (mOverflowBubbles.size() == mMaxOverflowBubbles + 1) { + // Remove oldest bubble. + Bubble oldest = mOverflowBubbles.get(mOverflowBubbles.size() - 1); + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "Overflow full. Remove: " + oldest); + } + mStateChange.bubbleRemoved(oldest, Bubbles.DISMISS_OVERFLOW_MAX_REACHED); + mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_MAX_REACHED); + mOverflowBubbles.remove(oldest); + mStateChange.removedOverflowBubble = oldest; + } + } + + public void dismissAll(@DismissReason int reason) { + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "dismissAll: reason=" + reason); + } + if (mBubbles.isEmpty()) { + return; + } + setExpandedInternal(false); + setSelectedBubbleInternal(null); + while (!mBubbles.isEmpty()) { + doRemove(mBubbles.get(0).getKey(), reason); + } + dispatchPendingChanges(); + } + + private void dispatchPendingChanges() { + if (mListener != null && mStateChange.anythingChanged()) { + mListener.applyUpdate(mStateChange); + } + mStateChange = new Update(mBubbles, mOverflowBubbles); + } + + /** + * Requests a change to the selected bubble. + * + * @param bubble the new selected bubble + */ + private void setSelectedBubbleInternal(@Nullable BubbleViewProvider bubble) { + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "setSelectedBubbleInternal: " + bubble); + } + if (!mShowingOverflow && Objects.equals(bubble, mSelectedBubble)) { + return; + } + // Otherwise, if we are showing the overflow menu, return to the previously selected bubble. + boolean isOverflow = bubble != null && BubbleOverflow.KEY.equals(bubble.getKey()); + if (bubble != null + && !mBubbles.contains(bubble) + && !mOverflowBubbles.contains(bubble) + && !isOverflow) { + Log.e(TAG, "Cannot select bubble which doesn't exist!" + + " (" + bubble + ") bubbles=" + mBubbles); + return; + } + if (mExpanded && bubble != null && !isOverflow) { + ((Bubble) bubble).markAsAccessedAt(mTimeSource.currentTimeMillis()); + } + mSelectedBubble = bubble; + mStateChange.selectedBubble = bubble; + mStateChange.selectionChanged = true; + } + + void setCurrentUserId(int uid) { + mCurrentUserId = uid; + } + + /** + * Logs the bubble UI event. + * + * @param provider The bubble view provider that is being interacted on. Null value indicates + * that the user interaction is not specific to one bubble. + * @param action The user interaction enum + * @param packageName SystemUI package + * @param bubbleCount Number of bubbles in the stack + * @param bubbleIndex Index of bubble in the stack + * @param normalX Normalized x position of the stack + * @param normalY Normalized y position of the stack + */ + void logBubbleEvent(@Nullable BubbleViewProvider provider, int action, String packageName, + int bubbleCount, int bubbleIndex, float normalX, float normalY) { + if (provider == null) { + mLogger.logStackUiChanged(packageName, action, bubbleCount, normalX, normalY); + } else if (provider.getKey().equals(BubbleOverflow.KEY)) { + if (action == FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED) { + mLogger.logShowOverflow(packageName, mCurrentUserId); + } + } else { + mLogger.logBubbleUiChanged((Bubble) provider, packageName, action, bubbleCount, normalX, + normalY, bubbleIndex); + } + } + + /** + * Requests a change to the expanded state. + * + * @param shouldExpand the new requested state + */ + private void setExpandedInternal(boolean shouldExpand) { + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "setExpandedInternal: shouldExpand=" + shouldExpand); + } + if (mExpanded == shouldExpand) { + return; + } + if (shouldExpand) { + if (mBubbles.isEmpty() && !mShowingOverflow) { + Log.e(TAG, "Attempt to expand stack when empty!"); + return; + } + if (mSelectedBubble == null) { + Log.e(TAG, "Attempt to expand stack without selected bubble!"); + return; + } + if (mSelectedBubble instanceof Bubble) { + ((Bubble) mSelectedBubble).markAsAccessedAt(mTimeSource.currentTimeMillis()); + } + mStateChange.orderChanged |= repackAll(); + } else if (!mBubbles.isEmpty()) { + // Apply ordering and grouping rules from expanded -> collapsed, then save + // the result. + mStateChange.orderChanged |= repackAll(); + // Save the state which should be returned to when expanded (with no other changes) + + if (mShowingOverflow) { + // Show previously selected bubble instead of overflow menu on next expansion. + if (!mSelectedBubble.getKey().equals(mOverflow.getKey())) { + setSelectedBubbleInternal(mSelectedBubble); + } else { + setSelectedBubbleInternal(mBubbles.get(0)); + } + } + if (mBubbles.indexOf(mSelectedBubble) > 0) { + // Move the selected bubble to the top while collapsed. + int index = mBubbles.indexOf(mSelectedBubble); + if (index != 0) { + mBubbles.remove((Bubble) mSelectedBubble); + mBubbles.add(0, (Bubble) mSelectedBubble); + mStateChange.orderChanged = true; + } + } + } + mExpanded = shouldExpand; + mStateChange.expanded = shouldExpand; + mStateChange.expandedChanged = true; + } + + private static long sortKey(Bubble bubble) { + return bubble.getLastActivity(); + } + + /** + * This applies a full sort and group pass to all existing bubbles. + * Bubbles are sorted by lastUpdated descending. + * + * @return true if the position of any bubbles changed as a result + */ + private boolean repackAll() { + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "repackAll()"); + } + if (mBubbles.isEmpty()) { + return false; + } + List<Bubble> repacked = new ArrayList<>(mBubbles.size()); + // Add bubbles, freshest to oldest + mBubbles.stream() + .sorted(BUBBLES_BY_SORT_KEY_DESCENDING) + .forEachOrdered(repacked::add); + if (repacked.equals(mBubbles)) { + return false; + } + mBubbles.clear(); + mBubbles.addAll(repacked); + return true; + } + + private void maybeSendDeleteIntent(@DismissReason int reason, @NonNull final Bubble bubble) { + if (reason != Bubbles.DISMISS_USER_GESTURE) return; + PendingIntent deleteIntent = bubble.getDeleteIntent(); + if (deleteIntent == null) return; + try { + deleteIntent.send(); + } catch (PendingIntent.CanceledException e) { + Log.w(TAG, "Failed to send delete intent for bubble with key: " + bubble.getKey()); + } + } + + private int indexForKey(String key) { + for (int i = 0; i < mBubbles.size(); i++) { + Bubble bubble = mBubbles.get(i); + if (bubble.getKey().equals(key)) { + return i; + } + } + return -1; + } + + /** + * The set of bubbles in row. + */ + @VisibleForTesting(visibility = PACKAGE) + public List<Bubble> getBubbles() { + return Collections.unmodifiableList(mBubbles); + } + + /** + * The set of bubbles in overflow. + */ + @VisibleForTesting(visibility = PRIVATE) + public List<Bubble> getOverflowBubbles() { + return Collections.unmodifiableList(mOverflowBubbles); + } + + @VisibleForTesting(visibility = PRIVATE) + @Nullable + Bubble getAnyBubbleWithkey(String key) { + Bubble b = getBubbleInStackWithKey(key); + if (b == null) { + b = getOverflowBubbleWithKey(key); + } + return b; + } + + @VisibleForTesting(visibility = PRIVATE) + @Nullable + public Bubble getBubbleInStackWithKey(String key) { + for (int i = 0; i < mBubbles.size(); i++) { + Bubble bubble = mBubbles.get(i); + if (bubble.getKey().equals(key)) { + return bubble; + } + } + return null; + } + + @Nullable + Bubble getBubbleWithView(View view) { + for (int i = 0; i < mBubbles.size(); i++) { + Bubble bubble = mBubbles.get(i); + if (bubble.getIconView() != null && bubble.getIconView().equals(view)) { + return bubble; + } + } + return null; + } + + @VisibleForTesting(visibility = PRIVATE) + public Bubble getOverflowBubbleWithKey(String key) { + for (int i = 0; i < mOverflowBubbles.size(); i++) { + Bubble bubble = mOverflowBubbles.get(i); + if (bubble.getKey().equals(key)) { + return bubble; + } + } + return null; + } + + @VisibleForTesting(visibility = PRIVATE) + void setTimeSource(TimeSource timeSource) { + mTimeSource = timeSource; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + /** + * Set maximum number of bubbles allowed in overflow. + * This method should only be used in tests, not in production. + */ + @VisibleForTesting + public void setMaxOverflowBubbles(int maxOverflowBubbles) { + mMaxOverflowBubbles = maxOverflowBubbles; + } + + /** + * Description of current bubble data state. + */ + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.print("selected: "); + pw.println(mSelectedBubble != null + ? mSelectedBubble.getKey() + : "null"); + pw.print("expanded: "); + pw.println(mExpanded); + + pw.print("stack bubble count: "); + pw.println(mBubbles.size()); + for (Bubble bubble : mBubbles) { + bubble.dump(fd, pw, args); + } + + pw.print("overflow bubble count: "); + pw.println(mOverflowBubbles.size()); + for (Bubble bubble : mOverflowBubbles) { + bubble.dump(fd, pw, args); + } + + pw.print("summaryKeys: "); + pw.println(mSuppressedGroupKeys.size()); + for (String key : mSuppressedGroupKeys.keySet()) { + pw.println(" suppressing: " + key); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt new file mode 100644 index 000000000000..3108b02cc010 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt @@ -0,0 +1,183 @@ +/* + * 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.bubbles + +import android.annotation.SuppressLint +import android.annotation.UserIdInt +import android.content.Context +import android.content.pm.LauncherApps +import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED +import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC +import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER +import android.os.UserHandle +import android.util.Log +import com.android.wm.shell.bubbles.storage.BubbleEntity +import com.android.wm.shell.bubbles.storage.BubblePersistentRepository +import com.android.wm.shell.bubbles.storage.BubbleVolatileRepository +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.common.annotations.ExternalThread +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.launch +import kotlinx.coroutines.yield + +internal class BubbleDataRepository(context: Context, private val launcherApps: LauncherApps, + private val mainExecutor : ShellExecutor) { + private val volatileRepository = BubbleVolatileRepository(launcherApps) + private val persistentRepository = BubblePersistentRepository(context) + + private val ioScope = CoroutineScope(Dispatchers.IO) + private var job: Job? = null + + /** + * Adds the bubble in memory, then persists the snapshot after adding the bubble to disk + * asynchronously. + */ + fun addBubble(@UserIdInt userId: Int, bubble: Bubble) = addBubbles(userId, listOf(bubble)) + + /** + * Adds the bubble in memory, then persists the snapshot after adding the bubble to disk + * asynchronously. + */ + fun addBubbles(@UserIdInt userId: Int, bubbles: List<Bubble>) { + if (DEBUG) Log.d(TAG, "adding ${bubbles.size} bubbles") + val entities = transform(userId, bubbles).also(volatileRepository::addBubbles) + if (entities.isNotEmpty()) persistToDisk() + } + + /** + * Removes the bubbles from memory, then persists the snapshot to disk asynchronously. + */ + fun removeBubbles(@UserIdInt userId: Int, bubbles: List<Bubble>) { + if (DEBUG) Log.d(TAG, "removing ${bubbles.size} bubbles") + val entities = transform(userId, bubbles).also(volatileRepository::removeBubbles) + if (entities.isNotEmpty()) persistToDisk() + } + + private fun transform(userId: Int, bubbles: List<Bubble>): List<BubbleEntity> { + return bubbles.mapNotNull { b -> + BubbleEntity( + userId, + b.packageName, + b.metadataShortcutId ?: return@mapNotNull null, + b.key, + b.rawDesiredHeight, + b.rawDesiredHeightResId, + b.title + ) + } + } + + /** + * Persists the bubbles to disk. When being called multiple times, it waits for first ongoing + * write operation to finish then run another write operation exactly once. + * + * e.g. + * Job A started -> blocking I/O + * Job B started, cancels A, wait for blocking I/O in A finishes + * Job C started, cancels B, wait for job B to finish + * Job D started, cancels C, wait for job C to finish + * Job A completed + * Job B resumes and reaches yield() and is then cancelled + * Job C resumes and reaches yield() and is then cancelled + * Job D resumes and performs another blocking I/O + */ + private fun persistToDisk() { + val prev = job + job = ioScope.launch { + // if there was an ongoing disk I/O operation, they can be cancelled + prev?.cancelAndJoin() + // check for cancellation before disk I/O + yield() + // save to disk + persistentRepository.persistsToDisk(volatileRepository.bubbles) + } + } + + /** + * Load bubbles from disk. + * @param cb The callback to be run after the bubbles are loaded. This callback is always made + * on the main thread of the hosting process. + */ + @SuppressLint("WrongConstant") + fun loadBubbles(cb: (List<Bubble>) -> Unit) = ioScope.launch { + /** + * Load BubbleEntity from disk. + * e.g. + * [ + * BubbleEntity(0, "com.example.messenger", "id-2"), + * BubbleEntity(10, "com.example.chat", "my-id1") + * BubbleEntity(0, "com.example.messenger", "id-1") + * ] + */ + val entities = persistentRepository.readFromDisk() + volatileRepository.addBubbles(entities) + /** + * Extract userId/packageName from these entities. + * e.g. + * [ + * ShortcutKey(0, "com.example.messenger"), ShortcutKey(0, "com.example.chat") + * ] + */ + val shortcutKeys = entities.map { ShortcutKey(it.userId, it.packageName) }.toSet() + /** + * Retrieve shortcuts with given userId/packageName combination, then construct a mapping + * from the userId/packageName pair to a list of associated ShortcutInfo. + * e.g. + * { + * ShortcutKey(0, "com.example.messenger") -> [ + * ShortcutInfo(userId=0, pkg="com.example.messenger", id="id-0"), + * ShortcutInfo(userId=0, pkg="com.example.messenger", id="id-2") + * ] + * ShortcutKey(10, "com.example.chat") -> [ + * ShortcutInfo(userId=10, pkg="com.example.chat", id="id-1"), + * ShortcutInfo(userId=10, pkg="com.example.chat", id="id-3") + * ] + * } + */ + val shortcutMap = shortcutKeys.flatMap { key -> + launcherApps.getShortcuts( + LauncherApps.ShortcutQuery() + .setPackage(key.pkg) + .setQueryFlags(SHORTCUT_QUERY_FLAG), UserHandle.of(key.userId)) + ?: emptyList() + }.groupBy { ShortcutKey(it.userId, it.`package`) } + // For each entity loaded from xml, find the corresponding ShortcutInfo then convert them + // into Bubble. + val bubbles = entities.mapNotNull { entity -> + shortcutMap[ShortcutKey(entity.userId, entity.packageName)] + ?.firstOrNull { shortcutInfo -> entity.shortcutId == shortcutInfo.id } + ?.let { shortcutInfo -> Bubble( + entity.key, + shortcutInfo, + entity.desiredHeight, + entity.desiredHeightResId, + entity.title, + mainExecutor + ) } + } + mainExecutor.execute { cb(bubbles) } + } +} + +data class ShortcutKey(val userId: Int, val pkg: String) + +private const val TAG = "BubbleDataRepository" +private const val DEBUG = false +private const val SHORTCUT_QUERY_FLAG = + FLAG_MATCH_DYNAMIC or FLAG_MATCH_PINNED_BY_ANY_LAUNCHER or FLAG_MATCH_CACHED
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java new file mode 100644 index 000000000000..dc2ace949f0c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java @@ -0,0 +1,82 @@ +/* + * 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.bubbles; + +import android.content.Context; +import android.provider.Settings; + +import java.util.List; + +/** + * Common class for the various debug {@link android.util.Log} output configuration in the Bubbles + * package. + */ +public class BubbleDebugConfig { + + // All output logs in the Bubbles package use the {@link #TAG_BUBBLES} string for tagging their + // log output. This makes it easy to identify the origin of the log message when sifting + // through a large amount of log output from multiple sources. However, it also makes trying + // to figure-out the origin of a log message while debugging the Bubbles a little painful. By + // setting this constant to true, log messages from the Bubbles package will be tagged with + // their class names instead fot the generic tag. + public static final boolean TAG_WITH_CLASS_NAME = false; + + // Default log tag for the Bubbles package. + public static final String TAG_BUBBLES = "Bubbles"; + + static final boolean DEBUG_BUBBLE_CONTROLLER = false; + static final boolean DEBUG_BUBBLE_DATA = false; + static final boolean DEBUG_BUBBLE_STACK_VIEW = false; + static final boolean DEBUG_BUBBLE_EXPANDED_VIEW = false; + static final boolean DEBUG_EXPERIMENTS = true; + static final boolean DEBUG_OVERFLOW = false; + static final boolean DEBUG_USER_EDUCATION = false; + static final boolean DEBUG_POSITIONER = false; + + private static final boolean FORCE_SHOW_USER_EDUCATION = false; + private static final String FORCE_SHOW_USER_EDUCATION_SETTING = + "force_show_bubbles_user_education"; + + /** + * @return whether we should force show user education for bubbles. Used for debugging & demos. + */ + static boolean forceShowUserEducation(Context context) { + boolean forceShow = Settings.Secure.getInt(context.getContentResolver(), + FORCE_SHOW_USER_EDUCATION_SETTING, 0) != 0; + return FORCE_SHOW_USER_EDUCATION || forceShow; + } + + static String formatBubblesString(List<Bubble> bubbles, BubbleViewProvider selected) { + StringBuilder sb = new StringBuilder(); + for (Bubble bubble : bubbles) { + if (bubble == null) { + sb.append(" <null> !!!!!\n"); + } else { + boolean isSelected = (selected != null + && selected.getKey() != BubbleOverflow.KEY + && bubble == selected); + String arrow = isSelected ? "=>" : " "; + sb.append(String.format("%s Bubble{act=%12d, showInShade=%d, key=%s}\n", + arrow, + bubble.getLastActivity(), + (bubble.showInShade() ? 1 : 0), + bubble.getKey())); + } + } + return sb.toString(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEntry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEntry.java new file mode 100644 index 000000000000..ff68861eb40c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEntry.java @@ -0,0 +1,134 @@ +/* + * 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.bubbles; + +import static android.app.Notification.FLAG_BUBBLE; + +import android.app.Notification; +import android.app.Notification.BubbleMetadata; +import android.app.NotificationManager.Policy; +import android.service.notification.NotificationListenerService.Ranking; +import android.service.notification.StatusBarNotification; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Represents a notification with needed data and flag for bubbles. + * + * @see Bubble + */ +public class BubbleEntry { + + private StatusBarNotification mSbn; + private Ranking mRanking; + + private boolean mIsClearable; + private boolean mShouldSuppressNotificationDot; + private boolean mShouldSuppressNotificationList; + private boolean mShouldSuppressPeek; + + public BubbleEntry(@NonNull StatusBarNotification sbn, + Ranking ranking, boolean isClearable, boolean shouldSuppressNotificationDot, + boolean shouldSuppressNotificationList, boolean shouldSuppressPeek) { + mSbn = sbn; + mRanking = ranking; + + mIsClearable = isClearable; + mShouldSuppressNotificationDot = shouldSuppressNotificationDot; + mShouldSuppressNotificationList = shouldSuppressNotificationList; + mShouldSuppressPeek = shouldSuppressPeek; + } + + /** @return the {@link StatusBarNotification} for this entry. */ + @NonNull + public StatusBarNotification getStatusBarNotification() { + return mSbn; + } + + /** @return the {@link Ranking} for this entry. */ + public Ranking getRanking() { + return mRanking; + } + + /** @return the key in the {@link StatusBarNotification}. */ + public String getKey() { + return mSbn.getKey(); + } + + /** @return the group key in the {@link StatusBarNotification}. */ + public String getGroupKey() { + return mSbn.getGroupKey(); + } + + /** @return the {@link BubbleMetadata} in the {@link StatusBarNotification}. */ + @Nullable + public BubbleMetadata getBubbleMetadata() { + return getStatusBarNotification().getNotification().getBubbleMetadata(); + } + + /** + * Updates the {@link Notification#FLAG_BUBBLE} flag on this notification to indicate + * whether it is a bubble or not. If this entry is set to not bubble, or does not have + * the required info to bubble, the flag cannot be set to true. + * + * @param shouldBubble whether this notification should be flagged as a bubble. + * @return true if the value changed. + */ + public boolean setFlagBubble(boolean shouldBubble) { + boolean wasBubble = isBubble(); + if (!shouldBubble) { + mSbn.getNotification().flags &= ~FLAG_BUBBLE; + } else if (getBubbleMetadata() != null && canBubble()) { + // wants to be bubble & can bubble, set flag + mSbn.getNotification().flags |= FLAG_BUBBLE; + } + return wasBubble != isBubble(); + } + + public boolean isBubble() { + return (mSbn.getNotification().flags & FLAG_BUBBLE) != 0; + } + + /** @see Ranking#canBubble() */ + public boolean canBubble() { + return mRanking.canBubble(); + } + + /** @return true if this notification is clearable. */ + public boolean isClearable() { + return mIsClearable; + } + + /** @return true if {@link Policy#SUPPRESSED_EFFECT_BADGE} set for this notification. */ + public boolean shouldSuppressNotificationDot() { + return mShouldSuppressNotificationDot; + } + + /** + * @return true if {@link Policy#SUPPRESSED_EFFECT_NOTIFICATION_LIST} + * set for this notification. + */ + public boolean shouldSuppressNotificationList() { + return mShouldSuppressNotificationList; + } + + /** @return true if {@link Policy#SUPPRESSED_EFFECT_PEEK} set for this notification. */ + public boolean shouldSuppressPeek() { + return mShouldSuppressPeek; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java new file mode 100644 index 000000000000..29458ef70e2d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -0,0 +1,694 @@ +/* + * 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.bubbles; + +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; + +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.app.ActivityOptions; +import android.app.ActivityTaskManager; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Outline; +import android.graphics.Picture; +import android.graphics.Rect; +import android.graphics.drawable.ShapeDrawable; +import android.os.RemoteException; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceControl; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewOutlineProvider; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import androidx.annotation.Nullable; + +import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.wm.shell.R; +import com.android.wm.shell.TaskView; +import com.android.wm.shell.common.AlphaOptimizedButton; +import com.android.wm.shell.common.TriangleShape; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * Container for the expanded bubble view, handles rendering the caret and settings icon. + */ +public class BubbleExpandedView extends LinearLayout { + private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES; + + // The triangle pointing to the expanded view + private View mPointerView; + private int mPointerMargin; + @Nullable private int[] mExpandedViewContainerLocation; + + private AlphaOptimizedButton mSettingsIcon; + private TaskView mTaskView; + private BubbleOverflowContainerView mOverflowView; + + private int mTaskId = INVALID_TASK_ID; + + private boolean mImeVisible; + private boolean mNeedsNewHeight; + + private int mMinHeight; + private int mOverflowHeight; + private int mSettingsIconHeight; + private int mPointerWidth; + private int mPointerHeight; + private ShapeDrawable mCurrentPointer; + private ShapeDrawable mTopPointer; + private ShapeDrawable mLeftPointer; + private ShapeDrawable mRightPointer; + private int mExpandedViewPadding; + private float mCornerRadius = 0f; + + @Nullable private Bubble mBubble; + private PendingIntent mPendingIntent; + // TODO(b/170891664): Don't use a flag, set the BubbleOverflow object instead + private boolean mIsOverflow; + + private BubbleController mController; + private BubbleStackView mStackView; + private BubblePositioner mPositioner; + + /** + * Container for the ActivityView that has a solid, round-rect background that shows if the + * ActivityView hasn't loaded. + */ + private final FrameLayout mExpandedViewContainer = new FrameLayout(getContext()); + + private final TaskView.Listener mTaskViewListener = new TaskView.Listener() { + private boolean mInitialized = false; + private boolean mDestroyed = false; + + @Override + public void onInitialized() { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onActivityViewReady: destroyed=" + mDestroyed + + " initialized=" + mInitialized + + " bubble=" + getBubbleKey()); + } + + if (mDestroyed || mInitialized) { + return; + } + + // Custom options so there is no activity transition animation + ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(), + 0 /* enterResId */, 0 /* exitResId */); + + // TODO: I notice inconsistencies in lifecycle + // Post to keep the lifecycle normal + post(() -> { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onActivityViewReady: calling startActivity, bubble=" + + getBubbleKey()); + } + try { + options.setTaskAlwaysOnTop(true); + if (!mIsOverflow && mBubble.hasMetadataShortcutId()) { + options.setApplyActivityFlagsForBubbles(true); + mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), + options, null /* sourceBounds */); + } else { + Intent fillInIntent = new Intent(); + // Apply flags to make behaviour match documentLaunchMode=always. + fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + if (mBubble != null) { + mBubble.setIntentActive(); + } + mTaskView.startActivity(mPendingIntent, fillInIntent, options); + } + } catch (RuntimeException e) { + // If there's a runtime exception here then there's something + // wrong with the intent, we can't really recover / try to populate + // the bubble again so we'll just remove it. + Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey() + + ", " + e.getMessage() + "; removing bubble"); + mController.removeBubble(getBubbleKey(), Bubbles.DISMISS_INVALID_INTENT); + } + }); + mInitialized = true; + } + + @Override + public void onReleased() { + mDestroyed = true; + } + + @Override + public void onTaskCreated(int taskId, ComponentName name) { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onTaskCreated: taskId=" + taskId + + " bubble=" + getBubbleKey()); + } + // The taskId is saved to use for removeTask, preventing appearance in recent tasks. + mTaskId = taskId; + + // With the task org, the taskAppeared callback will only happen once the task has + // already drawn + setContentVisibility(true); + } + + @Override + public void onTaskVisibilityChanged(int taskId, boolean visible) { + setContentVisibility(visible); + } + + @Override + public void onTaskRemovalStarted(int taskId) { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId + + " bubble=" + getBubbleKey()); + } + if (mBubble != null) { + // Must post because this is called from a binder thread. + post(() -> mController.removeBubble( + mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED)); + } + } + + @Override + public void onBackPressedOnTaskRoot(int taskId) { + if (mTaskId == taskId && mStackView.isExpanded()) { + mController.collapseStack(); + } + } + }; + + public BubbleExpandedView(Context context) { + this(context, null); + } + + public BubbleExpandedView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + updateDimensions(); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + Resources res = getResources(); + mPointerView = findViewById(R.id.pointer_view); + mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width); + mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height); + + mTopPointer = new ShapeDrawable(TriangleShape.create( + mPointerWidth, mPointerHeight, true /* pointUp */)); + mLeftPointer = new ShapeDrawable(TriangleShape.createHorizontal( + mPointerWidth, mPointerHeight, true /* pointLeft */)); + mRightPointer = new ShapeDrawable(TriangleShape.createHorizontal( + mPointerWidth, mPointerHeight, false /* pointLeft */)); + + mCurrentPointer = mTopPointer; + mPointerView.setVisibility(INVISIBLE); + + mSettingsIconHeight = getContext().getResources().getDimensionPixelSize( + R.dimen.bubble_manage_button_height); + mSettingsIcon = findViewById(R.id.settings_button); + + // Set ActivityView's alpha value as zero, since there is no view content to be shown. + setContentVisibility(false); + + mExpandedViewContainer.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius); + } + }); + mExpandedViewContainer.setClipToOutline(true); + mExpandedViewContainer.setLayoutParams( + new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + addView(mExpandedViewContainer); + + // Expanded stack layout, top to bottom: + // Expanded view container + // ==> bubble row + // ==> expanded view + // ==> activity view + // ==> manage button + bringChildToFront(mSettingsIcon); + + applyThemeAttrs(); + + mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); + setClipToPadding(false); + setOnTouchListener((view, motionEvent) -> { + if (mTaskView == null) { + return false; + } + + final Rect avBounds = new Rect(); + mTaskView.getBoundsOnScreen(avBounds); + + // Consume and ignore events on the expanded view padding that are within the + // ActivityView's vertical bounds. These events are part of a back gesture, and so they + // should not collapse the stack (which all other touches on areas around the AV would + // do). + if (motionEvent.getRawY() >= avBounds.top + && motionEvent.getRawY() <= avBounds.bottom + && (motionEvent.getRawX() < avBounds.left + || motionEvent.getRawX() > avBounds.right)) { + return true; + } + + return false; + }); + + // BubbleStackView is forced LTR, but we want to respect the locale for expanded view layout + // so the Manage button appears on the right. + setLayoutDirection(LAYOUT_DIRECTION_LOCALE); + } + + /** + * Initialize {@link BubbleController} and {@link BubbleStackView} here, this method must need + * to be called after view inflate. + */ + void initialize(BubbleController controller, BubbleStackView stackView, boolean isOverflow) { + mController = controller; + mStackView = stackView; + mIsOverflow = isOverflow; + mPositioner = mController.getPositioner(); + + if (mIsOverflow) { + mOverflowView = (BubbleOverflowContainerView) LayoutInflater.from(getContext()).inflate( + R.layout.bubble_overflow_container, null /* root */); + mOverflowView.setBubbleController(mController); + mExpandedViewContainer.addView(mOverflowView); + bringChildToFront(mOverflowView); + mSettingsIcon.setVisibility(GONE); + } else { + mTaskView = new TaskView(mContext, mController.getTaskOrganizer()); + mTaskView.setListener(mContext.getMainExecutor(), mTaskViewListener); + mExpandedViewContainer.addView(mTaskView); + bringChildToFront(mTaskView); + } + } + + void updateDimensions() { + Resources res = getResources(); + mMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height); + mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height); + mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin); + } + + void applyThemeAttrs() { + final TypedArray ta = mContext.obtainStyledAttributes(new int[] { + android.R.attr.dialogCornerRadius, + android.R.attr.colorBackgroundFloating}); + mCornerRadius = ta.getDimensionPixelSize(0, 0); + mExpandedViewContainer.setBackgroundColor(ta.getColor(1, Color.WHITE)); + ta.recycle(); + + if (mTaskView != null && ScreenDecorationsUtils.supportsRoundedCornersOnWindows( + mContext.getResources())) { + mTaskView.setCornerRadius(mCornerRadius); + } + updatePointerView(); + } + + private void updatePointerView() { + final int mode = + getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + switch (mode) { + case Configuration.UI_MODE_NIGHT_NO: + mCurrentPointer.setTint(getResources().getColor(R.color.bubbles_light)); + break; + case Configuration.UI_MODE_NIGHT_YES: + mCurrentPointer.setTint(getResources().getColor(R.color.bubbles_dark)); + break; + } + LayoutParams lp = (LayoutParams) mPointerView.getLayoutParams(); + if (mCurrentPointer == mLeftPointer || mCurrentPointer == mRightPointer) { + lp.width = mPointerHeight; + lp.height = mPointerWidth; + } else { + lp.width = mPointerWidth; + lp.height = mPointerHeight; + } + mPointerView.setLayoutParams(lp); + mPointerView.setBackground(mCurrentPointer); + } + + + private String getBubbleKey() { + return mBubble != null ? mBubble.getKey() : "null"; + } + + /** + * Sets whether the surface displaying app content should sit on top. This is useful for + * ordering surfaces during animations. When content is drawn on top of the app (e.g. bubble + * being dragged out, the manage menu) this is set to false, otherwise it should be true. + */ + void setSurfaceZOrderedOnTop(boolean onTop) { + if (mTaskView == null) { + return; + } + mTaskView.setZOrderedOnTop(onTop, true /* allowDynamicChange */); + } + + void setImeVisible(boolean visible) { + mImeVisible = visible; + if (!mImeVisible && mNeedsNewHeight) { + updateHeight(); + } + } + + /** Return a GraphicBuffer with the contents of the task view surface. */ + @Nullable + SurfaceControl.ScreenshotHardwareBuffer snapshotActivitySurface() { + if (mIsOverflow) { + // For now, just snapshot the view and return it as a hw buffer so that the animation + // code for both the tasks and overflow can be the same + Picture p = new Picture(); + mOverflowView.draw( + p.beginRecording(mOverflowView.getWidth(), mOverflowView.getHeight())); + p.endRecording(); + Bitmap snapshot = Bitmap.createBitmap(p); + return new SurfaceControl.ScreenshotHardwareBuffer(snapshot.getHardwareBuffer(), + snapshot.getColorSpace(), false /* containsSecureLayers */); + } + if (mTaskView == null || mTaskView.getSurfaceControl() == null) { + return null; + } + return SurfaceControl.captureLayers( + mTaskView.getSurfaceControl(), + new Rect(0, 0, mTaskView.getWidth(), mTaskView.getHeight()), + 1 /* scale */); + } + + int[] getTaskViewLocationOnScreen() { + if (mIsOverflow) { + // This is only used for animating away the surface when switching bubbles, just use the + // view location on screen for now to allow us to use the same animation code with tasks + return mOverflowView.getLocationOnScreen(); + } + if (mTaskView != null) { + return mTaskView.getLocationOnScreen(); + } else { + return new int[]{0, 0}; + } + } + + // TODO: Could listener be passed when we pass StackView / can we avoid setting this like this + void setManageClickListener(OnClickListener manageClickListener) { + mSettingsIcon.setOnClickListener(manageClickListener); + } + + /** + * Updates the obscured touchable region for the task surface. This calls onLocationChanged, + * which results in a call to {@link BubbleStackView#subtractObscuredTouchableRegion}. This is + * useful if a view has been added or removed from on top of the ActivityView, such as the + * manage menu. + */ + void updateObscuredTouchableRegion() { + if (mTaskView != null) { + mTaskView.onLocationChanged(); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mImeVisible = false; + mNeedsNewHeight = false; + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onDetachedFromWindow: bubble=" + getBubbleKey()); + } + } + + /** + * Set visibility of contents in the expanded state. + * + * @param visibility {@code true} if the contents should be visible on the screen. + * + * Note that this contents visibility doesn't affect visibility at {@link android.view.View}, + * and setting {@code false} actually means rendering the contents in transparent. + */ + void setContentVisibility(boolean visibility) { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "setContentVisibility: visibility=" + visibility + + " bubble=" + getBubbleKey()); + } + final float alpha = visibility ? 1f : 0f; + + mPointerView.setAlpha(alpha); + if (mTaskView != null) { + mTaskView.setAlpha(alpha); + } + } + + @Nullable + View getTaskView() { + return mTaskView; + } + + int getTaskId() { + return mTaskId; + } + + /** + * Sets the bubble used to populate this view. + */ + void update(Bubble bubble) { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "update: bubble=" + bubble); + } + if (mStackView == null) { + Log.w(TAG, "Stack is null for bubble: " + bubble); + return; + } + boolean isNew = mBubble == null || didBackingContentChange(bubble); + if (isNew || bubble != null && bubble.getKey().equals(mBubble.getKey())) { + mBubble = bubble; + mSettingsIcon.setContentDescription(getResources().getString( + R.string.bubbles_settings_button_description, bubble.getAppName())); + mSettingsIcon.setAccessibilityDelegate( + new AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, + AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + // On focus, have TalkBack say + // "Actions available. Use swipe up then right to view." + // in addition to the default "double tap to activate". + mStackView.setupLocalMenu(info); + } + }); + + if (isNew) { + mPendingIntent = mBubble.getBubbleIntent(); + if ((mPendingIntent != null || mBubble.hasMetadataShortcutId()) + && mTaskView != null) { + setContentVisibility(false); + mTaskView.setVisibility(VISIBLE); + } + } + applyThemeAttrs(); + } else { + Log.w(TAG, "Trying to update entry with different key, new bubble: " + + bubble.getKey() + " old bubble: " + bubble.getKey()); + } + } + + /** + * Bubbles are backed by a pending intent or a shortcut, once the activity is + * started we never change it / restart it on notification updates -- unless the bubbles' + * backing data switches. + * + * This indicates if the new bubble is backed by a different data source than what was + * previously shown here (e.g. previously a pending intent & now a shortcut). + * + * @param newBubble the bubble this view is being updated with. + * @return true if the backing content has changed. + */ + private boolean didBackingContentChange(Bubble newBubble) { + boolean prevWasIntentBased = mBubble != null && mPendingIntent != null; + boolean newIsIntentBased = newBubble.getBubbleIntent() != null; + return prevWasIntentBased != newIsIntentBased; + } + + void updateHeight() { + if (mExpandedViewContainerLocation == null) { + return; + } + + if (mBubble != null || mIsOverflow) { + float desiredHeight = mIsOverflow + ? mOverflowHeight + : mBubble.getDesiredHeight(mContext); + desiredHeight = Math.max(desiredHeight, mMinHeight); + float height = Math.min(desiredHeight, getMaxExpandedHeight()); + height = Math.max(height, mMinHeight); + FrameLayout.LayoutParams lp = mIsOverflow + ? (FrameLayout.LayoutParams) mOverflowView.getLayoutParams() + : (FrameLayout.LayoutParams) mTaskView.getLayoutParams(); + mNeedsNewHeight = lp.height != height; + if (!mImeVisible) { + // If the ime is visible... don't adjust the height because that will cause + // a configuration change and the ime will be lost. + lp.height = (int) height; + if (mIsOverflow) { + mOverflowView.setLayoutParams(lp); + } else { + mTaskView.setLayoutParams(lp); + } + mNeedsNewHeight = false; + } + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "updateHeight: bubble=" + getBubbleKey() + + " height=" + height + + " mNeedsNewHeight=" + mNeedsNewHeight); + } + } + } + + private int getMaxExpandedHeight() { + int expandedContainerY = mExpandedViewContainerLocation != null + // Remove top insets back here because availableRect.height would account for that + ? mExpandedViewContainerLocation[1] - mPositioner.getInsets().top + : 0; + return mPositioner.getAvailableRect().height() + - expandedContainerY + - getPaddingTop() + - getPaddingBottom() + - mSettingsIconHeight + - mPointerHeight + - mPointerMargin; + } + + /** + * Update appearance of the expanded view being displayed. + * + * @param containerLocationOnScreen The location on-screen of the container the expanded view is + * added to. This allows us to calculate max height without + * waiting for layout. + */ + public void updateView(int[] containerLocationOnScreen) { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "updateView: bubble=" + + getBubbleKey()); + } + mExpandedViewContainerLocation = containerLocationOnScreen; + if (mTaskView != null + && mTaskView.getVisibility() == VISIBLE + && mTaskView.isAttachedToWindow()) { + updateHeight(); + mTaskView.onLocationChanged(); + } + if (mIsOverflow) { + mOverflowView.show(); + } + } + + /** + * Set the position that the tip of the triangle should point to. + */ + public void setPointerPosition(float x, float y, boolean isLandscape, boolean onLeft) { + // Pointer gets drawn in the padding + int paddingLeft = (isLandscape && onLeft) ? mPointerHeight : 0; + int paddingRight = (isLandscape && !onLeft) ? mPointerHeight : 0; + int paddingTop = isLandscape ? 0 : mExpandedViewPadding; + setPadding(paddingLeft, paddingTop, paddingRight, 0); + + if (isLandscape) { + // TODO: why setY vs setTranslationY ? linearlayout? + mPointerView.setY(y - (mPointerWidth / 2f)); + mPointerView.setTranslationX(onLeft ? -mPointerHeight : x - mExpandedViewPadding); + } else { + mPointerView.setTranslationY(0f); + mPointerView.setTranslationX(x - mExpandedViewPadding - (mPointerWidth / 2f)); + } + mCurrentPointer = isLandscape ? onLeft ? mLeftPointer : mRightPointer : mTopPointer; + updatePointerView(); + mPointerView.setVisibility(VISIBLE); + } + + /** + * Position of the manage button displayed in the expanded view. Used for placing user + * education about the manage button. + */ + public void getManageButtonBoundsOnScreen(Rect rect) { + mSettingsIcon.getBoundsOnScreen(rect); + } + + /** + * Cleans up anything related to the task and TaskView. If this view should be reused after this + * method is called, then {@link #initialize(BubbleController, BubbleStackView, boolean)} must + * be invoked first. + */ + public void cleanUpExpandedState() { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId); + } + if (getTaskId() != INVALID_TASK_ID) { + try { + ActivityTaskManager.getService().removeTask(getTaskId()); + } catch (RemoteException e) { + Log.w(TAG, e.getMessage()); + } + } + if (mTaskView != null) { + mTaskView.release(); + removeView(mTaskView); + mTaskView = null; + } + } + + /** + * Description of current expanded view state. + */ + public void dump( + @NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { + pw.print("BubbleExpandedView"); + pw.print(" taskId: "); pw.println(mTaskId); + pw.print(" stackView: "); pw.println(mStackView); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java new file mode 100644 index 000000000000..19c3cf9c462a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java @@ -0,0 +1,526 @@ +/* + * 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.bubbles; + +import static android.graphics.Paint.ANTI_ALIAS_FLAG; +import static android.graphics.Paint.FILTER_BITMAP_FLAG; + +import static com.android.wm.shell.animation.Interpolators.ALPHA_IN; +import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT; + +import android.animation.ArgbEvaluator; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Outline; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PointF; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.ShapeDrawable; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewOutlineProvider; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.Nullable; + +import com.android.wm.shell.R; +import com.android.wm.shell.common.TriangleShape; + +/** + * Flyout view that appears as a 'chat bubble' alongside the bubble stack. The flyout can visually + * transform into the 'new' dot, which is used during flyout dismiss animations/gestures. + */ +public class BubbleFlyoutView extends FrameLayout { + /** Max width of the flyout, in terms of percent of the screen width. */ + private static final float FLYOUT_MAX_WIDTH_PERCENT = .6f; + + /** Translation Y of fade animation. */ + private static final float FLYOUT_FADE_Y = 40f; + + private static final long FLYOUT_FADE_OUT_DURATION = 150L; + private static final long FLYOUT_FADE_IN_DURATION = 250L; + + private final int mFlyoutPadding; + private final int mFlyoutSpaceFromBubble; + private final int mPointerSize; + private int mBubbleSize; + private int mBubbleBitmapSize; + + private final int mFlyoutElevation; + private final int mBubbleElevation; + private final int mFloatingBackgroundColor; + private final float mCornerRadius; + + private final ViewGroup mFlyoutTextContainer; + private final ImageView mSenderAvatar; + private final TextView mSenderText; + private final TextView mMessageText; + + /** Values related to the 'new' dot which we use to figure out where to collapse the flyout. */ + private float mNewDotRadius; + private float mNewDotSize; + private float mOriginalDotSize; + + /** + * The paint used to draw the background, whose color changes as the flyout transitions to the + * tinted 'new' dot. + */ + private final Paint mBgPaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG); + private final ArgbEvaluator mArgbEvaluator = new ArgbEvaluator(); + + /** + * Triangular ShapeDrawables used for the triangle that points from the flyout to the bubble + * stack (a chat-bubble effect). + */ + private final ShapeDrawable mLeftTriangleShape; + private final ShapeDrawable mRightTriangleShape; + + /** Whether the flyout arrow is on the left (pointing left) or right (pointing right). */ + private boolean mArrowPointingLeft = true; + + /** Color of the 'new' dot that the flyout will transform into. */ + private int mDotColor; + + /** The outline of the triangle, used for elevation shadows. */ + private final Outline mTriangleOutline = new Outline(); + + /** The bounds of the flyout background, kept up to date as it transitions to the 'new' dot. */ + private final RectF mBgRect = new RectF(); + + /** The y position of the flyout, relative to the top of the screen. */ + private float mFlyoutY = 0f; + + /** + * Percent progress in the transition from flyout to 'new' dot. These two values are the inverse + * of each other (if we're 40% transitioned to the dot, we're 60% flyout), but it makes the code + * much more readable. + */ + private float mPercentTransitionedToDot = 1f; + private float mPercentStillFlyout = 0f; + + /** + * The difference in values between the flyout and the dot. These differences are gradually + * added over the course of the animation to transform the flyout into the 'new' dot. + */ + private float mFlyoutToDotWidthDelta = 0f; + private float mFlyoutToDotHeightDelta = 0f; + + /** The translation values when the flyout is completely transitioned into the dot. */ + private float mTranslationXWhenDot = 0f; + private float mTranslationYWhenDot = 0f; + + /** + * The current translation values applied to the flyout background as it transitions into the + * 'new' dot. + */ + private float mBgTranslationX; + private float mBgTranslationY; + + private float[] mDotCenter; + + /** The flyout's X translation when at rest (not animating or dragging). */ + private float mRestingTranslationX = 0f; + + /** The badge sizes are defined as percentages of the app icon size. Same value as Launcher3. */ + private static final float SIZE_PERCENTAGE = 0.228f; + + private static final float DOT_SCALE = 1f; + + /** Callback to run when the flyout is hidden. */ + @Nullable private Runnable mOnHide; + + public BubbleFlyoutView(Context context) { + super(context); + LayoutInflater.from(context).inflate(R.layout.bubble_flyout, this, true); + + mFlyoutTextContainer = findViewById(R.id.bubble_flyout_text_container); + mSenderText = findViewById(R.id.bubble_flyout_name); + mSenderAvatar = findViewById(R.id.bubble_flyout_avatar); + mMessageText = mFlyoutTextContainer.findViewById(R.id.bubble_flyout_text); + + final Resources res = getResources(); + mFlyoutPadding = res.getDimensionPixelSize(R.dimen.bubble_flyout_padding_x); + mFlyoutSpaceFromBubble = res.getDimensionPixelSize(R.dimen.bubble_flyout_space_from_bubble); + mPointerSize = res.getDimensionPixelSize(R.dimen.bubble_flyout_pointer_size); + + mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); + mFlyoutElevation = res.getDimensionPixelSize(R.dimen.bubble_flyout_elevation); + + final TypedArray ta = mContext.obtainStyledAttributes( + new int[] { + android.R.attr.colorBackgroundFloating, + android.R.attr.dialogCornerRadius}); + mFloatingBackgroundColor = ta.getColor(0, Color.WHITE); + mCornerRadius = ta.getDimensionPixelSize(1, 0); + ta.recycle(); + + // Add padding for the pointer on either side, onDraw will draw it in this space. + setPadding(mPointerSize, 0, mPointerSize, 0); + setWillNotDraw(false); + setClipChildren(false); + setTranslationZ(mFlyoutElevation); + setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + BubbleFlyoutView.this.getOutline(outline); + } + }); + + // Use locale direction so the text is aligned correctly. + setLayoutDirection(LAYOUT_DIRECTION_LOCALE); + + mBgPaint.setColor(mFloatingBackgroundColor); + + mLeftTriangleShape = + new ShapeDrawable(TriangleShape.createHorizontal( + mPointerSize, mPointerSize, true /* isPointingLeft */)); + mLeftTriangleShape.setBounds(0, 0, mPointerSize, mPointerSize); + mLeftTriangleShape.getPaint().setColor(mFloatingBackgroundColor); + + mRightTriangleShape = + new ShapeDrawable(TriangleShape.createHorizontal( + mPointerSize, mPointerSize, false /* isPointingLeft */)); + mRightTriangleShape.setBounds(0, 0, mPointerSize, mPointerSize); + mRightTriangleShape.getPaint().setColor(mFloatingBackgroundColor); + } + + @Override + protected void onDraw(Canvas canvas) { + renderBackground(canvas); + invalidateOutline(); + super.onDraw(canvas); + } + + void updateFontSize(float fontScale) { + final float fontSize = mContext.getResources() + .getDimensionPixelSize(com.android.internal.R.dimen.text_size_body_2_material); + final float newFontSize = fontSize * fontScale; + mMessageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, newFontSize); + mSenderText.setTextSize(TypedValue.COMPLEX_UNIT_PX, newFontSize); + } + + /* + * Fade animation for consecutive flyouts. + */ + void animateUpdate(Bubble.FlyoutMessage flyoutMessage, float parentWidth, float stackY) { + final Runnable afterFadeOut = () -> { + updateFlyoutMessage(flyoutMessage, parentWidth); + // Wait for TextViews to layout with updated height. + post(() -> { + mFlyoutY = stackY + (mBubbleSize - mFlyoutTextContainer.getHeight()) / 2f; + fade(true /* in */, () -> {} /* after */); + } /* after */ ); + }; + fade(false /* in */, afterFadeOut); + } + + /* + * Fade-out above or fade-in from below. + */ + private void fade(boolean in, Runnable afterFade) { + setAlpha(in ? 0f : 1f); + setTranslationY(in ? mFlyoutY + FLYOUT_FADE_Y : mFlyoutY); + animate() + .alpha(in ? 1f : 0f) + .setDuration(in ? FLYOUT_FADE_IN_DURATION : FLYOUT_FADE_OUT_DURATION) + .setInterpolator(in ? ALPHA_IN : ALPHA_OUT); + animate() + .translationY(in ? mFlyoutY : mFlyoutY - FLYOUT_FADE_Y) + .setDuration(in ? FLYOUT_FADE_IN_DURATION : FLYOUT_FADE_OUT_DURATION) + .setInterpolator(in ? ALPHA_IN : ALPHA_OUT) + .withEndAction(afterFade); + } + + private void updateFlyoutMessage(Bubble.FlyoutMessage flyoutMessage, float parentWidth) { + final Drawable senderAvatar = flyoutMessage.senderAvatar; + if (senderAvatar != null && flyoutMessage.isGroupChat) { + mSenderAvatar.setVisibility(VISIBLE); + mSenderAvatar.setImageDrawable(senderAvatar); + } else { + mSenderAvatar.setVisibility(GONE); + mSenderAvatar.setTranslationX(0); + mMessageText.setTranslationX(0); + mSenderText.setTranslationX(0); + } + + final int maxTextViewWidth = + (int) (parentWidth * FLYOUT_MAX_WIDTH_PERCENT) - mFlyoutPadding * 2; + + // Name visibility + if (!TextUtils.isEmpty(flyoutMessage.senderName)) { + mSenderText.setMaxWidth(maxTextViewWidth); + mSenderText.setText(flyoutMessage.senderName); + mSenderText.setVisibility(VISIBLE); + } else { + mSenderText.setVisibility(GONE); + } + + // Set the flyout TextView's max width in terms of percent, and then subtract out the + // padding so that the entire flyout view will be the desired width (rather than the + // TextView being the desired width + extra padding). + mMessageText.setMaxWidth(maxTextViewWidth); + mMessageText.setText(flyoutMessage.message); + } + + /** Configures the flyout, collapsed into dot form. */ + void setupFlyoutStartingAsDot( + Bubble.FlyoutMessage flyoutMessage, + PointF stackPos, + float parentWidth, + boolean arrowPointingLeft, + int dotColor, + @Nullable Runnable onLayoutComplete, + @Nullable Runnable onHide, + float[] dotCenter, + boolean hideDot, + BubblePositioner positioner) { + + mBubbleBitmapSize = positioner.getBubbleBitmapSize(); + mBubbleSize = positioner.getBubbleSize(); + + mOriginalDotSize = SIZE_PERCENTAGE * mBubbleBitmapSize; + mNewDotRadius = (DOT_SCALE * mOriginalDotSize) / 2f; + mNewDotSize = mNewDotRadius * 2f; + + updateFlyoutMessage(flyoutMessage, parentWidth); + + mArrowPointingLeft = arrowPointingLeft; + mDotColor = dotColor; + mOnHide = onHide; + mDotCenter = dotCenter; + + setCollapsePercent(1f); + + // Wait for TextViews to layout with updated height. + post(() -> { + // Flyout is vertically centered with respect to the bubble. + mFlyoutY = + stackPos.y + (mBubbleSize - mFlyoutTextContainer.getHeight()) / 2f; + setTranslationY(mFlyoutY); + + // Calculate the translation required to position the flyout next to the bubble stack, + // with the desired padding. + mRestingTranslationX = mArrowPointingLeft + ? stackPos.x + mBubbleSize + mFlyoutSpaceFromBubble + : stackPos.x - getWidth() - mFlyoutSpaceFromBubble; + + // Calculate the difference in size between the flyout and the 'dot' so that we can + // transform into the dot later. + final float newDotSize = hideDot ? 0f : mNewDotSize; + mFlyoutToDotWidthDelta = getWidth() - newDotSize; + mFlyoutToDotHeightDelta = getHeight() - newDotSize; + + // Calculate the translation values needed to be in the correct 'new dot' position. + final float adjustmentForScaleAway = hideDot ? 0f : (mOriginalDotSize / 2f); + final float dotPositionX = stackPos.x + mDotCenter[0] - adjustmentForScaleAway; + final float dotPositionY = stackPos.y + mDotCenter[1] - adjustmentForScaleAway; + + final float distanceFromFlyoutLeftToDotCenterX = mRestingTranslationX - dotPositionX; + final float distanceFromLayoutTopToDotCenterY = mFlyoutY - dotPositionY; + + mTranslationXWhenDot = -distanceFromFlyoutLeftToDotCenterX; + mTranslationYWhenDot = -distanceFromLayoutTopToDotCenterY; + if (onLayoutComplete != null) { + onLayoutComplete.run(); + } + }); + } + + /** + * Hides the flyout and runs the optional callback passed into setupFlyoutStartingAsDot. + * The flyout has been animated into the 'new' dot by the time we call this, so no animations + * are needed. + */ + void hideFlyout() { + if (mOnHide != null) { + mOnHide.run(); + mOnHide = null; + } + + setVisibility(GONE); + } + + /** Sets the percentage that the flyout should be collapsed into dot form. */ + void setCollapsePercent(float percentCollapsed) { + // This is unlikely, but can happen in a race condition where the flyout view hasn't been + // laid out and returns 0 for getWidth(). We check for this condition at the sites where + // this method is called, but better safe than sorry. + if (Float.isNaN(percentCollapsed)) { + return; + } + + mPercentTransitionedToDot = Math.max(0f, Math.min(percentCollapsed, 1f)); + mPercentStillFlyout = (1f - mPercentTransitionedToDot); + + // Move and fade out the text. + final float translationX = mPercentTransitionedToDot + * (mArrowPointingLeft ? -getWidth() : getWidth()); + final float alpha = clampPercentage( + (mPercentStillFlyout - (1f - BubbleStackView.FLYOUT_DRAG_PERCENT_DISMISS)) + / BubbleStackView.FLYOUT_DRAG_PERCENT_DISMISS); + + mMessageText.setTranslationX(translationX); + mMessageText.setAlpha(alpha); + + mSenderText.setTranslationX(translationX); + mSenderText.setAlpha(alpha); + + mSenderAvatar.setTranslationX(translationX); + mSenderAvatar.setAlpha(alpha); + + // Reduce the elevation towards that of the topmost bubble. + setTranslationZ( + mFlyoutElevation + - (mFlyoutElevation - mBubbleElevation) * mPercentTransitionedToDot); + invalidate(); + } + + /** Return the flyout's resting X translation (translation when not dragging or animating). */ + float getRestingTranslationX() { + return mRestingTranslationX; + } + + /** Clamps a float to between 0 and 1. */ + private float clampPercentage(float percent) { + return Math.min(1f, Math.max(0f, percent)); + } + + /** + * Renders the background, which is either the rounded 'chat bubble' flyout, or some state + * between that and the 'new' dot over the bubbles. + */ + private void renderBackground(Canvas canvas) { + // Calculate the width, height, and corner radius of the flyout given the current collapsed + // percentage. + final float width = getWidth() - (mFlyoutToDotWidthDelta * mPercentTransitionedToDot); + final float height = getHeight() - (mFlyoutToDotHeightDelta * mPercentTransitionedToDot); + final float interpolatedRadius = getInterpolatedRadius(); + + // Translate the flyout background towards the collapsed 'dot' state. + mBgTranslationX = mTranslationXWhenDot * mPercentTransitionedToDot; + mBgTranslationY = mTranslationYWhenDot * mPercentTransitionedToDot; + + // Set the bounds of the rounded rectangle that serves as either the flyout background or + // the collapsed 'dot'. These bounds will also be used to provide the outline for elevation + // shadows. In the expanded flyout state, the left and right bounds leave space for the + // pointer triangle - as the flyout collapses, this space is reduced since the triangle + // retracts into the flyout. + mBgRect.set( + mPointerSize * mPercentStillFlyout /* left */, + 0 /* top */, + width - mPointerSize * mPercentStillFlyout /* right */, + height /* bottom */); + + mBgPaint.setColor( + (int) mArgbEvaluator.evaluate( + mPercentTransitionedToDot, mFloatingBackgroundColor, mDotColor)); + + canvas.save(); + canvas.translate(mBgTranslationX, mBgTranslationY); + renderPointerTriangle(canvas, width, height); + canvas.drawRoundRect(mBgRect, interpolatedRadius, interpolatedRadius, mBgPaint); + canvas.restore(); + } + + /** Renders the 'pointer' triangle that points from the flyout to the bubble stack. */ + private void renderPointerTriangle( + Canvas canvas, float currentFlyoutWidth, float currentFlyoutHeight) { + canvas.save(); + + // Translation to apply for the 'retraction' effect as the flyout collapses. + final float retractionTranslationX = + (mArrowPointingLeft ? 1 : -1) * (mPercentTransitionedToDot * mPointerSize * 2f); + + // Place the arrow either at the left side, or the far right, depending on whether the + // flyout is on the left or right side. + final float arrowTranslationX = + mArrowPointingLeft + ? retractionTranslationX + : currentFlyoutWidth - mPointerSize + retractionTranslationX; + + // Vertically center the arrow at all times. + final float arrowTranslationY = currentFlyoutHeight / 2f - mPointerSize / 2f; + + // Draw the appropriate direction of arrow. + final ShapeDrawable relevantTriangle = + mArrowPointingLeft ? mLeftTriangleShape : mRightTriangleShape; + canvas.translate(arrowTranslationX, arrowTranslationY); + relevantTriangle.setAlpha((int) (255f * mPercentStillFlyout)); + relevantTriangle.draw(canvas); + + // Save the triangle's outline for use in the outline provider, offsetting it to reflect its + // current position. + relevantTriangle.getOutline(mTriangleOutline); + mTriangleOutline.offset((int) arrowTranslationX, (int) arrowTranslationY); + + canvas.restore(); + } + + /** Builds an outline that includes the transformed flyout background and triangle. */ + private void getOutline(Outline outline) { + if (!mTriangleOutline.isEmpty()) { + // Draw the rect into the outline as a path so we can merge the triangle path into it. + final Path rectPath = new Path(); + final float interpolatedRadius = getInterpolatedRadius(); + rectPath.addRoundRect(mBgRect, interpolatedRadius, + interpolatedRadius, Path.Direction.CW); + outline.setPath(rectPath); + + // Get rid of the triangle path once it has disappeared behind the flyout. + if (mPercentStillFlyout > 0.5f) { + outline.mPath.addPath(mTriangleOutline.mPath); + } + + // Translate the outline to match the background's position. + final Matrix outlineMatrix = new Matrix(); + outlineMatrix.postTranslate(getLeft() + mBgTranslationX, getTop() + mBgTranslationY); + + // At the very end, retract the outline into the bubble so the shadow will be pulled + // into the flyout-dot as it (visually) becomes part of the bubble. We can't do this by + // animating translationZ to zero since then it'll go under the bubbles, which have + // elevation. + if (mPercentTransitionedToDot > 0.98f) { + final float percentBetween99and100 = (mPercentTransitionedToDot - 0.98f) / .02f; + final float percentShadowVisible = 1f - percentBetween99and100; + + // Keep it centered. + outlineMatrix.postTranslate( + mNewDotRadius * percentBetween99and100, + mNewDotRadius * percentBetween99and100); + outlineMatrix.preScale(percentShadowVisible, percentShadowVisible); + } + + outline.mPath.transform(outlineMatrix); + } + } + + private float getInterpolatedRadius() { + return mNewDotRadius * mPercentTransitionedToDot + + mCornerRadius * (1 - mPercentTransitionedToDot); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java new file mode 100644 index 000000000000..2d9da215efb7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java @@ -0,0 +1,158 @@ +/* + * 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.bubbles; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.Intent; +import android.content.pm.LauncherApps; +import android.content.pm.ShortcutInfo; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; + +import com.android.launcher3.icons.BaseIconFactory; +import com.android.launcher3.icons.BitmapInfo; +import com.android.launcher3.icons.ShadowGenerator; +import com.android.wm.shell.R; + +/** + * Factory for creating normalized bubble icons. + * We are not using Launcher's IconFactory because bubbles only runs on the UI thread, + * so there is no need to manage a pool across multiple threads. + */ +public class BubbleIconFactory extends BaseIconFactory { + + private int mBadgeSize; + + protected BubbleIconFactory(Context context) { + super(context, context.getResources().getConfiguration().densityDpi, + context.getResources().getDimensionPixelSize(R.dimen.individual_bubble_size)); + mBadgeSize = mContext.getResources().getDimensionPixelSize( + com.android.launcher3.icons.R.dimen.profile_badge_size); + } + + /** + * Returns the drawable that the developer has provided to display in the bubble. + */ + Drawable getBubbleDrawable(@NonNull final Context context, + @Nullable final ShortcutInfo shortcutInfo, @Nullable final Icon ic) { + if (shortcutInfo != null) { + LauncherApps launcherApps = + (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE); + int density = context.getResources().getConfiguration().densityDpi; + return launcherApps.getShortcutIconDrawable(shortcutInfo, density); + } else { + if (ic != null) { + if (ic.getType() == Icon.TYPE_URI + || ic.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) { + context.grantUriPermission(context.getPackageName(), + ic.getUri(), + Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + return ic.loadDrawable(context); + } + return null; + } + } + + /** + * Returns a {@link BitmapInfo} for the app-badge that is shown on top of each bubble. This + * will include the workprofile indicator on the badge if appropriate. + */ + BitmapInfo getBadgeBitmap(Drawable userBadgedAppIcon, boolean isImportantConversation) { + ShadowGenerator shadowGenerator = new ShadowGenerator(mBadgeSize); + Bitmap userBadgedBitmap = createIconBitmap(userBadgedAppIcon, 1f, mBadgeSize); + + if (userBadgedAppIcon instanceof AdaptiveIconDrawable) { + userBadgedBitmap = Bitmap.createScaledBitmap( + getCircleBitmap((AdaptiveIconDrawable) userBadgedAppIcon, /* size */ + userBadgedAppIcon.getIntrinsicWidth()), + mBadgeSize, mBadgeSize, /* filter */ true); + } + + if (isImportantConversation) { + final float ringStrokeWidth = mContext.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.importance_ring_stroke_width); + final int importantConversationColor = mContext.getResources().getColor( + com.android.settingslib.R.color.important_conversation, null); + Bitmap badgeAndRing = Bitmap.createBitmap(userBadgedBitmap.getWidth(), + userBadgedBitmap.getHeight(), userBadgedBitmap.getConfig()); + Canvas c = new Canvas(badgeAndRing); + + final int bitmapTop = (int) ringStrokeWidth; + final int bitmapLeft = (int) ringStrokeWidth; + final int bitmapWidth = c.getWidth() - 2 * (int) ringStrokeWidth; + final int bitmapHeight = c.getHeight() - 2 * (int) ringStrokeWidth; + + Bitmap scaledBitmap = Bitmap.createScaledBitmap(userBadgedBitmap, bitmapWidth, + bitmapHeight, /* filter */ true); + c.drawBitmap(scaledBitmap, bitmapTop, bitmapLeft, /* paint */null); + + Paint ringPaint = new Paint(); + ringPaint.setStyle(Paint.Style.STROKE); + ringPaint.setColor(importantConversationColor); + ringPaint.setAntiAlias(true); + ringPaint.setStrokeWidth(ringStrokeWidth); + c.drawCircle(c.getWidth() / 2, c.getHeight() / 2, c.getWidth() / 2 - ringStrokeWidth, + ringPaint); + + shadowGenerator.recreateIcon(Bitmap.createBitmap(badgeAndRing), c); + return createIconBitmap(badgeAndRing); + } else { + Canvas c = new Canvas(); + c.setBitmap(userBadgedBitmap); + shadowGenerator.recreateIcon(Bitmap.createBitmap(userBadgedBitmap), c); + return createIconBitmap(userBadgedBitmap); + } + } + + public Bitmap getCircleBitmap(AdaptiveIconDrawable icon, int size) { + Drawable foreground = icon.getForeground(); + Drawable background = icon.getBackground(); + Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(); + canvas.setBitmap(bitmap); + + // Clip canvas to circle. + Path circlePath = new Path(); + circlePath.addCircle(/* x */ size / 2f, + /* y */ size / 2f, + /* radius */ size / 2f, + Path.Direction.CW); + canvas.clipPath(circlePath); + + // Draw background. + background.setBounds(0, 0, size, size); + background.draw(canvas); + + // Draw foreground. The foreground and background drawables are derived from adaptive icons + // Some icon shapes fill more space than others, so adaptive icons are normalized to about + // the same size. This size is smaller than the original bounds, so we estimate + // the difference in this offset. + int offset = size / 5; + foreground.setBounds(-offset, -offset, size + offset, size + offset); + foreground.draw(canvas); + + canvas.setBitmap(null); + return bitmap; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java new file mode 100644 index 000000000000..c88a58be1461 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.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.bubbles; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; +import com.android.internal.util.FrameworkStatsLog; + +/** + * Implementation of UiEventLogger for logging bubble UI events. + * + * See UiEventReported atom in atoms.proto for more context. + */ +public class BubbleLogger { + + private final UiEventLogger mUiEventLogger; + + /** + * Bubble UI event. + */ + @VisibleForTesting + public enum Event implements UiEventLogger.UiEventEnum { + + @UiEvent(doc = "User dismissed the bubble via gesture, add bubble to overflow.") + BUBBLE_OVERFLOW_ADD_USER_GESTURE(483), + + @UiEvent(doc = "No more space in top row, add bubble to overflow.") + BUBBLE_OVERFLOW_ADD_AGED(484), + + @UiEvent(doc = "No more space in overflow, remove bubble from overflow") + BUBBLE_OVERFLOW_REMOVE_MAX_REACHED(485), + + @UiEvent(doc = "Notification canceled, remove bubble from overflow.") + BUBBLE_OVERFLOW_REMOVE_CANCEL(486), + + @UiEvent(doc = "Notification group canceled, remove bubble for child notif from overflow.") + BUBBLE_OVERFLOW_REMOVE_GROUP_CANCEL(487), + + @UiEvent(doc = "Notification no longer bubble, remove bubble from overflow.") + BUBBLE_OVERFLOW_REMOVE_NO_LONGER_BUBBLE(488), + + @UiEvent(doc = "User tapped overflow bubble. Promote bubble back to top row.") + BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK(489), + + @UiEvent(doc = "User blocked notification from bubbling, remove bubble from overflow.") + BUBBLE_OVERFLOW_REMOVE_BLOCKED(490), + + @UiEvent(doc = "User selected the overflow.") + BUBBLE_OVERFLOW_SELECTED(600), + + @UiEvent(doc = "Restore bubble to overflow after phone reboot.") + BUBBLE_OVERFLOW_RECOVER(691); + + private final int mId; + + Event(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } + } + + public BubbleLogger(UiEventLogger uiEventLogger) { + mUiEventLogger = uiEventLogger; + } + + /** + * @param b Bubble involved in this UI event + * @param e UI event + */ + public void log(Bubble b, UiEventLogger.UiEventEnum e) { + mUiEventLogger.logWithInstanceId(e, b.getAppUid(), b.getPackageName(), b.getInstanceId()); + } + + /** + * @param b Bubble removed from overflow + * @param r Reason that bubble was removed + */ + public void logOverflowRemove(Bubble b, @Bubbles.DismissReason int r) { + if (r == Bubbles.DISMISS_NOTIF_CANCEL) { + log(b, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_CANCEL); + } else if (r == Bubbles.DISMISS_GROUP_CANCELLED) { + log(b, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_GROUP_CANCEL); + } else if (r == Bubbles.DISMISS_NO_LONGER_BUBBLE) { + log(b, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_NO_LONGER_BUBBLE); + } else if (r == Bubbles.DISMISS_BLOCKED) { + log(b, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BLOCKED); + } + } + + /** + * @param b Bubble added to overflow + * @param r Reason that bubble was added to overflow + */ + public void logOverflowAdd(Bubble b, @Bubbles.DismissReason int r) { + if (r == Bubbles.DISMISS_AGED) { + log(b, Event.BUBBLE_OVERFLOW_ADD_AGED); + } else if (r == Bubbles.DISMISS_USER_GESTURE) { + log(b, Event.BUBBLE_OVERFLOW_ADD_USER_GESTURE); + } else if (r == Bubbles.DISMISS_RELOAD_FROM_DISK) { + log(b, Event.BUBBLE_OVERFLOW_RECOVER); + } + } + + void logStackUiChanged(String packageName, int action, int bubbleCount, float normalX, + float normalY) { + FrameworkStatsLog.write(FrameworkStatsLog.BUBBLE_UI_CHANGED, + packageName, + null /* notification channel */, + 0 /* notification ID */, + 0 /* bubble position */, + bubbleCount, + action, + normalX, + normalY, + false /* unread bubble */, + false /* on-going bubble */, + false /* isAppForeground (unused) */); + } + + void logShowOverflow(String packageName, int currentUserId) { + mUiEventLogger.log(BubbleLogger.Event.BUBBLE_OVERFLOW_SELECTED, currentUserId, + packageName); + } + + void logBubbleUiChanged(Bubble bubble, String packageName, int action, int bubbleCount, + float normalX, float normalY, int index) { + FrameworkStatsLog.write(FrameworkStatsLog.BUBBLE_UI_CHANGED, + packageName, + bubble.getChannelId() /* notification channel */, + bubble.getNotificationId() /* notification ID */, + index, + bubbleCount, + action, + normalX, + normalY, + bubble.showInShade() /* isUnread */, + false /* isOngoing (unused) */, + false /* isAppForeground (unused) */); + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt new file mode 100644 index 000000000000..16cd3cf3686c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt @@ -0,0 +1,199 @@ +/* + * 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.bubbles + +import android.app.ActivityTaskManager.INVALID_TASK_ID +import android.content.Context +import android.content.res.Configuration +import android.graphics.Bitmap +import android.graphics.Matrix +import android.graphics.Path +import android.graphics.drawable.AdaptiveIconDrawable +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.InsetDrawable +import android.util.PathParser +import android.util.TypedValue +import android.view.LayoutInflater +import android.widget.FrameLayout +import com.android.wm.shell.R + +/** + * The icon in the bubble overflow is scaled down, this is the percent of the normal bubble bitmap + * size to use. + */ +const val ICON_BITMAP_SIZE_PERCENT = 0.46f + +class BubbleOverflow( + private val context: Context, + private val positioner: BubblePositioner +) : BubbleViewProvider { + + private lateinit var bitmap: Bitmap + private lateinit var dotPath: Path + + private var bitmapSize = 0 + private var iconBitmapSize = 0 + private var dotColor = 0 + private var showDot = false + + private val inflater: LayoutInflater = LayoutInflater.from(context) + private var expandedView: BubbleExpandedView? + private var overflowBtn: BadgedImageView? + + init { + updateResources() + bitmapSize = positioner.bubbleBitmapSize + iconBitmapSize = (bitmapSize * ICON_BITMAP_SIZE_PERCENT).toInt() + expandedView = null + overflowBtn = null + } + + /** Call before use and again if cleanUpExpandedState was called. */ + fun initialize(controller: BubbleController) { + getExpandedView()?.initialize(controller, controller.stackView, true /* isOverflow */) + } + + fun cleanUpExpandedState() { + expandedView?.cleanUpExpandedState() + expandedView = null + } + + fun update() { + updateResources() + getExpandedView()?.applyThemeAttrs() + // Apply inset and new style to fresh icon drawable. + getIconView()?.setImageResource(R.drawable.bubble_ic_overflow_button) + updateBtnTheme() + } + + fun updateResources() { + bitmapSize = positioner.bubbleBitmapSize + iconBitmapSize = (bitmapSize * 0.46f).toInt() + val bubbleSize = positioner.bubbleSize + overflowBtn?.layoutParams = FrameLayout.LayoutParams(bubbleSize, bubbleSize) + expandedView?.updateDimensions() + } + + private fun updateBtnTheme() { + val res = context.resources + + // Set overflow button accent color, dot color + val typedValue = TypedValue() + context.theme.resolveAttribute(android.R.attr.colorAccent, typedValue, true) + val colorAccent = res.getColor(typedValue.resourceId, null) + overflowBtn?.drawable?.setTint(colorAccent) + dotColor = colorAccent + + val iconFactory = BubbleIconFactory(context) + + // Update bitmap + val nightMode = (res.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + == Configuration.UI_MODE_NIGHT_YES) + val bg = ColorDrawable(res.getColor( + if (nightMode) R.color.bubbles_dark else R.color.bubbles_light, null)) + + val fg = InsetDrawable(overflowBtn?.drawable, + bitmapSize - iconBitmapSize /* inset */) + bitmap = iconFactory.createBadgedIconBitmap(AdaptiveIconDrawable(bg, fg), + null /* user */, true /* shrinkNonAdaptiveIcons */).icon + + // Update dot path + dotPath = PathParser.createPathFromPathData( + res.getString(com.android.internal.R.string.config_icon_mask)) + val scale = iconFactory.normalizer.getScale(iconView!!.drawable, + null /* outBounds */, null /* path */, null /* outMaskShape */) + val radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f + val matrix = Matrix() + matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */, + radius /* pivot y */) + dotPath.transform(matrix) + + // Attach BubbleOverflow to BadgedImageView + overflowBtn?.setRenderedBubble(this) + overflowBtn?.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE) + } + + fun setVisible(visible: Int) { + overflowBtn?.visibility = visible + } + + fun setShowDot(show: Boolean) { + showDot = show + overflowBtn?.updateDotVisibility(true /* animate */) + } + + override fun getExpandedView(): BubbleExpandedView? { + if (expandedView == null) { + expandedView = inflater.inflate(R.layout.bubble_expanded_view, + null /* root */, false /* attachToRoot */) as BubbleExpandedView + expandedView?.applyThemeAttrs() + updateResources() + } + return expandedView + } + + override fun getDotColor(): Int { + return dotColor + } + + override fun getAppBadge(): Drawable? { + return null + } + + override fun getBubbleIcon(): Bitmap { + return bitmap + } + + override fun showDot(): Boolean { + return showDot + } + + override fun getDotPath(): Path? { + return dotPath + } + + override fun setContentVisibility(visible: Boolean) { + expandedView?.setContentVisibility(visible) + } + + override fun getIconView(): BadgedImageView? { + if (overflowBtn == null) { + overflowBtn = inflater.inflate(R.layout.bubble_overflow_button, + null /* root */, false /* attachToRoot */) as BadgedImageView + overflowBtn?.initialize(positioner) + overflowBtn?.contentDescription = context.resources.getString( + R.string.bubble_overflow_button_content_description) + val bubbleSize = positioner.bubbleSize + overflowBtn?.layoutParams = FrameLayout.LayoutParams(bubbleSize, bubbleSize) + updateBtnTheme() + } + return overflowBtn + } + + override fun getKey(): String { + return KEY + } + + override fun getTaskId(): Int { + return if (expandedView != null) expandedView!!.taskId else INVALID_TASK_ID + } + + companion object { + const val KEY = "Overflow" + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java new file mode 100644 index 000000000000..39e4e1a09019 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java @@ -0,0 +1,362 @@ +/* + * 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.bubbles; + +import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_OVERFLOW; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.os.Bundle; +import android.os.IBinder; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.internal.util.ContrastColorUtil; +import com.android.wm.shell.R; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * Container view for showing aged out bubbles. + */ +public class BubbleOverflowContainerView extends LinearLayout { + static final String EXTRA_BUBBLE_CONTROLLER = "bubble_controller"; + + private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleOverflowActivity" : TAG_BUBBLES; + + private LinearLayout mEmptyState; + private TextView mEmptyStateTitle; + private TextView mEmptyStateSubtitle; + private ImageView mEmptyStateImage; + private BubbleController mController; + private BubbleOverflowAdapter mAdapter; + private RecyclerView mRecyclerView; + private List<Bubble> mOverflowBubbles = new ArrayList<>(); + + private class NoScrollGridLayoutManager extends GridLayoutManager { + NoScrollGridLayoutManager(Context context, int columns) { + super(context, columns); + } + @Override + public boolean canScrollVertically() { + if (getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE) { + return super.canScrollVertically(); + } + return false; + } + + @Override + public int getColumnCountForAccessibility(RecyclerView.Recycler recycler, + RecyclerView.State state) { + int bubbleCount = state.getItemCount(); + int columnCount = super.getColumnCountForAccessibility(recycler, state); + if (bubbleCount < columnCount) { + // If there are 4 columns and bubbles <= 3, + // TalkBack says "AppName 1 of 4 in list 4 items" + // This is a workaround until TalkBack bug is fixed for GridLayoutManager + return bubbleCount; + } + return columnCount; + } + } + + public BubbleOverflowContainerView(Context context) { + super(context); + } + + public BubbleOverflowContainerView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public BubbleOverflowContainerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public BubbleOverflowContainerView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public void setBubbleController(BubbleController controller) { + mController = controller; + } + + public void show() { + setVisibility(View.VISIBLE); + updateOverflow(); + } + + public void hide() { + setVisibility(View.INVISIBLE); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mRecyclerView = findViewById(R.id.bubble_overflow_recycler); + mEmptyState = findViewById(R.id.bubble_overflow_empty_state); + mEmptyStateTitle = findViewById(R.id.bubble_overflow_empty_title); + mEmptyStateSubtitle = findViewById(R.id.bubble_overflow_empty_subtitle); + mEmptyStateImage = findViewById(R.id.bubble_overflow_empty_state_image); + } + + void updateOverflow() { + Resources res = getResources(); + final int columns = res.getInteger(R.integer.bubbles_overflow_columns); + mRecyclerView.setLayoutManager( + new NoScrollGridLayoutManager(getContext(), columns)); + + DisplayMetrics displayMetrics = new DisplayMetrics(); + getContext().getDisplay().getMetrics(displayMetrics); + + final int overflowPadding = res.getDimensionPixelSize(R.dimen.bubble_overflow_padding); + final int recyclerViewWidth = displayMetrics.widthPixels - (overflowPadding * 2); + final int viewWidth = recyclerViewWidth / columns; + + final int maxOverflowBubbles = res.getInteger(R.integer.bubbles_max_overflow); + final int rows = (int) Math.ceil((double) maxOverflowBubbles / columns); + final int recyclerViewHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height) + - res.getDimensionPixelSize(R.dimen.bubble_overflow_padding); + final int viewHeight = recyclerViewHeight / rows; + + mAdapter = new BubbleOverflowAdapter(getContext(), mOverflowBubbles, + mController::promoteBubbleFromOverflow, + mController.getPositioner(), + viewWidth, viewHeight); + mRecyclerView.setAdapter(mAdapter); + + mOverflowBubbles.clear(); + mOverflowBubbles.addAll(mController.getOverflowBubbles()); + mAdapter.notifyDataSetChanged(); + + // Currently BubbleExpandedView.mExpandedViewContainer is WRAP_CONTENT so use the same + // width we would use for the recycler view + LayoutParams lp = (LayoutParams) mEmptyState.getLayoutParams(); + lp.width = recyclerViewWidth; + updateEmptyStateVisibility(); + + mController.setOverflowListener(mDataListener); + updateTheme(); + } + + void updateEmptyStateVisibility() { + if (mOverflowBubbles.isEmpty()) { + mEmptyState.setVisibility(View.VISIBLE); + } else { + mEmptyState.setVisibility(View.GONE); + } + } + + /** + * Handle theme changes. + */ + void updateTheme() { + Resources res = getResources(); + final int mode = res.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + final boolean isNightMode = (mode == Configuration.UI_MODE_NIGHT_YES); + + mEmptyStateImage.setImageDrawable(isNightMode + ? res.getDrawable(R.drawable.bubble_ic_empty_overflow_dark) + : res.getDrawable(R.drawable.bubble_ic_empty_overflow_light)); + + findViewById(R.id.bubble_overflow_container) + .setBackgroundColor(isNightMode + ? res.getColor(R.color.bubbles_dark) + : res.getColor(R.color.bubbles_light)); + + final TypedArray typedArray = getContext().obtainStyledAttributes( + new int[]{android.R.attr.colorBackgroundFloating, + android.R.attr.textColorSecondary}); + int bgColor = typedArray.getColor(0, isNightMode ? Color.BLACK : Color.WHITE); + int textColor = typedArray.getColor(1, isNightMode ? Color.WHITE : Color.BLACK); + textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, isNightMode); + typedArray.recycle(); + + mEmptyStateTitle.setTextColor(textColor); + mEmptyStateSubtitle.setTextColor(textColor); + } + + private final BubbleData.Listener mDataListener = new BubbleData.Listener() { + + @Override + public void applyUpdate(BubbleData.Update update) { + + Bubble toRemove = update.removedOverflowBubble; + if (toRemove != null) { + if (DEBUG_OVERFLOW) { + Log.d(TAG, "remove: " + toRemove); + } + toRemove.cleanupViews(); + final int i = mOverflowBubbles.indexOf(toRemove); + mOverflowBubbles.remove(toRemove); + mAdapter.notifyItemRemoved(i); + } + + Bubble toAdd = update.addedOverflowBubble; + if (toAdd != null) { + if (DEBUG_OVERFLOW) { + Log.d(TAG, "add: " + toAdd); + } + mOverflowBubbles.add(0, toAdd); + mAdapter.notifyItemInserted(0); + } + + updateEmptyStateVisibility(); + + if (DEBUG_OVERFLOW) { + Log.d(TAG, BubbleDebugConfig.formatBubblesString( + mController.getOverflowBubbles(), null)); + } + } + }; +} + +class BubbleOverflowAdapter extends RecyclerView.Adapter<BubbleOverflowAdapter.ViewHolder> { + private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleOverflowAdapter" : TAG_BUBBLES; + + private Context mContext; + private Consumer<Bubble> mPromoteBubbleFromOverflow; + private BubblePositioner mPositioner; + private List<Bubble> mBubbles; + private int mWidth; + private int mHeight; + + BubbleOverflowAdapter(Context context, + List<Bubble> list, + Consumer<Bubble> promoteBubble, + BubblePositioner positioner, + int width, int height) { + mContext = context; + mBubbles = list; + mPromoteBubbleFromOverflow = promoteBubble; + mPositioner = positioner; + mWidth = width; + mHeight = height; + } + + @Override + public BubbleOverflowAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, + int viewType) { + + // Set layout for overflow bubble view. + LinearLayout overflowView = (LinearLayout) LayoutInflater.from(parent.getContext()) + .inflate(R.layout.bubble_overflow_view, parent, false); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + params.width = mWidth; + params.height = mHeight; + overflowView.setLayoutParams(params); + + // Ensure name has enough contrast. + final TypedArray ta = mContext.obtainStyledAttributes( + new int[]{android.R.attr.colorBackgroundFloating, android.R.attr.textColorPrimary}); + final int bgColor = ta.getColor(0, Color.WHITE); + int textColor = ta.getColor(1, Color.BLACK); + textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true); + ta.recycle(); + + TextView viewName = overflowView.findViewById(R.id.bubble_view_name); + viewName.setTextColor(textColor); + + return new ViewHolder(overflowView, mPositioner); + } + + @Override + public void onBindViewHolder(ViewHolder vh, int index) { + Bubble b = mBubbles.get(index); + + vh.iconView.setRenderedBubble(b); + vh.iconView.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); + vh.iconView.setOnClickListener(view -> { + mBubbles.remove(b); + notifyDataSetChanged(); + mPromoteBubbleFromOverflow.accept(b); + }); + + String titleStr = b.getTitle(); + if (titleStr == null) { + titleStr = mContext.getResources().getString(R.string.notification_bubble_title); + } + vh.iconView.setContentDescription(mContext.getResources().getString( + R.string.bubble_content_description_single, titleStr, b.getAppName())); + + vh.iconView.setAccessibilityDelegate( + new View.AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, + AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + // Talkback prompts "Double tap to add back to stack" + // instead of the default "Double tap to activate" + info.addAction( + new AccessibilityNodeInfo.AccessibilityAction( + AccessibilityNodeInfo.ACTION_CLICK, + mContext.getResources().getString( + R.string.bubble_accessibility_action_add_back))); + } + }); + + CharSequence label = b.getShortcutInfo() != null + ? b.getShortcutInfo().getLabel() + : b.getAppName(); + vh.textView.setText(label); + } + + @Override + public int getItemCount() { + return mBubbles.size(); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + public BadgedImageView iconView; + public TextView textView; + + ViewHolder(LinearLayout v, BubblePositioner positioner) { + super(v); + iconView = v.findViewById(R.id.bubble_view); + iconView.initialize(positioner); + textView = v.findViewById(R.id.bubble_view_name); + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java new file mode 100644 index 000000000000..1562e4bf6fcc --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -0,0 +1,289 @@ +/* + * 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.bubbles; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.annotation.IntDef; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Insets; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.Log; +import android.view.View; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; + +import androidx.annotation.VisibleForTesting; + +import com.android.wm.shell.R; + +import java.lang.annotation.Retention; + +/** + * Keeps track of display size, configuration, and specific bubble sizes. One place for all + * placement and positioning calculations to refer to. + */ +public class BubblePositioner { + private static final String TAG = BubbleDebugConfig.TAG_WITH_CLASS_NAME + ? "BubblePositioner" + : BubbleDebugConfig.TAG_BUBBLES; + + @Retention(SOURCE) + @IntDef({TASKBAR_POSITION_NONE, TASKBAR_POSITION_RIGHT, TASKBAR_POSITION_LEFT, + TASKBAR_POSITION_BOTTOM}) + @interface TaskbarPosition {} + public static final int TASKBAR_POSITION_NONE = -1; + public static final int TASKBAR_POSITION_RIGHT = 0; + public static final int TASKBAR_POSITION_LEFT = 1; + public static final int TASKBAR_POSITION_BOTTOM = 2; + + /** + * The bitmap in the bubble is slightly smaller than the overall size of the bubble. + * This is the percentage to scale the image down based on the overall bubble size. + */ + private static final float BUBBLE_BITMAP_SIZE_PERCENT = 0.86f; + + private Context mContext; + private WindowManager mWindowManager; + private Rect mPositionRect; + private int mOrientation; + private Insets mInsets; + + private int mBubbleSize; + private int mBubbleBitmapSize; + + private PointF mPinLocation; + private PointF mRestingStackPosition; + + private boolean mShowingInTaskbar; + private @TaskbarPosition int mTaskbarPosition = TASKBAR_POSITION_NONE; + private int mTaskbarIconSize; + private int mTaskbarSize; + + public BubblePositioner(Context context, WindowManager windowManager) { + mContext = context; + mWindowManager = windowManager; + update(Configuration.ORIENTATION_UNDEFINED); + } + + /** + * Updates orientation, available space, and inset information. Call this when config changes + * occur or when added to a window. + */ + public void update(int orientation) { + WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics(); + if (windowMetrics == null) { + return; + } + WindowInsets metricInsets = windowMetrics.getWindowInsets(); + + Insets insets = metricInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars() + | WindowInsets.Type.statusBars() + | WindowInsets.Type.displayCutout()); + + if (BubbleDebugConfig.DEBUG_POSITIONER) { + Log.w(TAG, "update positioner:" + + " landscape= " + (orientation == Configuration.ORIENTATION_LANDSCAPE) + + " insets: " + insets + + " bounds: " + windowMetrics.getBounds() + + " showingInTaskbar: " + mShowingInTaskbar); + } + updateInternal(orientation, insets, windowMetrics.getBounds()); + } + + /** + * Updates position information to account for taskbar state. + * + * @param taskbarPosition which position the taskbar is displayed in. + * @param showingInTaskbar whether the taskbar is being shown. + */ + public void updateForTaskbar(int iconSize, + @TaskbarPosition int taskbarPosition, boolean showingInTaskbar, int taskbarSize) { + mShowingInTaskbar = showingInTaskbar; + mTaskbarIconSize = iconSize; + mTaskbarPosition = taskbarPosition; + mTaskbarSize = taskbarSize; + update(mOrientation); + } + + @VisibleForTesting + public void updateInternal(int orientation, Insets insets, Rect bounds) { + mOrientation = orientation; + mInsets = insets; + + mPositionRect = new Rect(bounds); + mPositionRect.left += mInsets.left; + mPositionRect.top += mInsets.top; + mPositionRect.right -= mInsets.right; + mPositionRect.bottom -= mInsets.bottom; + + Resources res = mContext.getResources(); + mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size); + mBubbleBitmapSize = res.getDimensionPixelSize(R.dimen.bubble_bitmap_size); + if (mShowingInTaskbar) { + adjustForTaskbar(); + } + } + + /** + * Taskbar insets appear as navigationBar insets, however, unlike navigationBar this should + * not inset bubbles UI as bubbles floats above the taskbar. This adjust the available space + * and insets to account for the taskbar. + */ + // TODO(b/171559950): When the insets are reported correctly we can remove this logic + private void adjustForTaskbar() { + // When bar is showing on edges... subtract that inset because we appear on top + if (mShowingInTaskbar && mTaskbarPosition != TASKBAR_POSITION_BOTTOM) { + WindowInsets metricInsets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); + Insets navBarInsets = metricInsets.getInsetsIgnoringVisibility( + WindowInsets.Type.navigationBars()); + int newInsetLeft = mInsets.left; + int newInsetRight = mInsets.right; + if (mTaskbarPosition == TASKBAR_POSITION_LEFT) { + mPositionRect.left -= navBarInsets.left; + newInsetLeft -= navBarInsets.left; + } else if (mTaskbarPosition == TASKBAR_POSITION_RIGHT) { + mPositionRect.right += navBarInsets.right; + newInsetRight -= navBarInsets.right; + } + mInsets = Insets.of(newInsetLeft, mInsets.top, newInsetRight, mInsets.bottom); + } + } + + /** + * @return a rect of available screen space accounting for orientation, system bars and cutouts. + * Does not account for IME. + */ + public Rect getAvailableRect() { + return mPositionRect; + } + + /** + * @return the relevant insets (status bar, nav bar, cutouts). If taskbar is showing, its + * inset is not included here. + */ + public Insets getInsets() { + return mInsets; + } + + /** + * @return whether the device is in landscape orientation. + */ + public boolean isLandscape() { + return mOrientation == Configuration.ORIENTATION_LANDSCAPE; + } + + /** + * Indicates how bubbles appear when expanded. + * + * When false, bubbles display at the top of the screen with the expanded view + * below them. When true, bubbles display at the edges of the screen with the expanded view + * to the left or right side. + */ + public boolean showBubblesVertically() { + return mOrientation == Configuration.ORIENTATION_LANDSCAPE + || mShowingInTaskbar; + } + + /** Size of the bubble account for badge & dot. */ + public int getBubbleSize() { + int bsize = (mShowingInTaskbar && mTaskbarIconSize > 0) + ? mTaskbarIconSize + : mBubbleSize; + return bsize; + } + + /** Size of the bitmap within the bubble */ + public int getBubbleBitmapSize() { + float size = (mShowingInTaskbar && mTaskbarIconSize > 0) + ? (mTaskbarIconSize * BUBBLE_BITMAP_SIZE_PERCENT) + : mBubbleBitmapSize; + return (int) size; + } + + /** + * Sets the stack's most recent position along the edge of the screen. This is saved when the + * last bubble is removed, so that the stack can be restored in its previous position. + */ + public void setRestingPosition(PointF position) { + if (mRestingStackPosition == null) { + mRestingStackPosition = new PointF(position); + } else { + mRestingStackPosition.set(position); + } + } + + /** The position the bubble stack should rest at when collapsed. */ + public PointF getRestingPosition() { + if (mPinLocation != null) { + return mPinLocation; + } + if (mRestingStackPosition == null) { + return getDefaultStartPosition(); + } + return mRestingStackPosition; + } + + /** + * @return the stack position to use if we don't have a saved location or if user education + * is being shown. + */ + public PointF getDefaultStartPosition() { + // Start on the left if we're in LTR, right otherwise. + final boolean startOnLeft = + mContext.getResources().getConfiguration().getLayoutDirection() + != View.LAYOUT_DIRECTION_RTL; + final float startingVerticalOffset = mContext.getResources().getDimensionPixelOffset( + R.dimen.bubble_stack_starting_offset_y); + // TODO: placement bug here because mPositionRect doesn't handle the overhanging edge + return new BubbleStackView.RelativeStackPosition( + startOnLeft, + startingVerticalOffset / mPositionRect.height()) + .getAbsolutePositionInRegion(new RectF(mPositionRect)); + } + + /** + * @return whether the bubble stack is pinned to the taskbar. + */ + public boolean showingInTaskbar() { + return mShowingInTaskbar; + } + + /** + * @return the taskbar position if set. + */ + public int getTaskbarPosition() { + return mTaskbarPosition; + } + + public int getTaskbarSize() { + return mTaskbarSize; + } + + /** + * In some situations bubbles will be pinned to a specific onscreen location. This sets the + * location to anchor the stack to. + */ + public void setPinnedLocation(PointF point) { + mPinLocation = point; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java new file mode 100644 index 000000000000..af421facd72a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -0,0 +1,2801 @@ +/* + * 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.bubbles; + +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.Insets; +import android.graphics.Outline; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.os.Bundle; +import android.os.Handler; +import android.provider.Settings; +import android.util.Log; +import android.view.Choreographer; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.SurfaceControl; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewOutlineProvider; +import android.view.ViewTreeObserver; +import android.view.WindowInsets; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.dynamicanimation.animation.FloatPropertyCompat; +import androidx.dynamicanimation.animation.SpringAnimation; +import androidx.dynamicanimation.animation.SpringForce; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FrameworkStatsLog; +import com.android.wm.shell.R; +import com.android.wm.shell.animation.Interpolators; +import com.android.wm.shell.animation.PhysicsAnimator; +import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix; +import com.android.wm.shell.bubbles.animation.ExpandedAnimationController; +import com.android.wm.shell.bubbles.animation.PhysicsAnimationLayout; +import com.android.wm.shell.bubbles.animation.StackAnimationController; +import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * Renders bubbles in a stack and handles animating expanded and collapsed states. + */ +public class BubbleStackView extends FrameLayout + implements ViewTreeObserver.OnComputeInternalInsetsListener { + private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES; + + /** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */ + static final float FLYOUT_DRAG_PERCENT_DISMISS = 0.25f; + + /** Velocity required to dismiss the flyout via drag. */ + private static final float FLYOUT_DISMISS_VELOCITY = 2000f; + + /** + * Factor for attenuating translation when the flyout is overscrolled (8f = flyout moves 1 pixel + * for every 8 pixels overscrolled). + */ + private static final float FLYOUT_OVERSCROLL_ATTENUATION_FACTOR = 8f; + + /** Duration of the flyout alpha animations. */ + private static final int FLYOUT_ALPHA_ANIMATION_DURATION = 100; + + private static final int FADE_IN_DURATION = 320; + + /** Percent to darken the bubbles when they're in the dismiss target. */ + private static final float DARKEN_PERCENT = 0.3f; + + /** How long to wait, in milliseconds, before hiding the flyout. */ + @VisibleForTesting + static final int FLYOUT_HIDE_AFTER = 5000; + + /** + * How long to wait to animate the stack temporarily invisible after a drag/flyout hide + * animation ends, if we are in fact temporarily invisible. + */ + private static final int ANIMATE_TEMPORARILY_INVISIBLE_DELAY = 1000; + + private static final PhysicsAnimator.SpringConfig FLYOUT_IME_ANIMATION_SPRING_CONFIG = + new PhysicsAnimator.SpringConfig( + StackAnimationController.IME_ANIMATION_STIFFNESS, + StackAnimationController.DEFAULT_BOUNCINESS); + + private final PhysicsAnimator.SpringConfig mScaleInSpringConfig = + new PhysicsAnimator.SpringConfig(300f, 0.9f); + + private final PhysicsAnimator.SpringConfig mScaleOutSpringConfig = + new PhysicsAnimator.SpringConfig(900f, 1f); + + private final PhysicsAnimator.SpringConfig mTranslateSpringConfig = + new PhysicsAnimator.SpringConfig( + SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_NO_BOUNCY); + + /** + * Handler to use for all delayed animations - this way, we can easily cancel them before + * starting a new animation. + */ + private final ShellExecutor mDelayedAnimationExecutor; + + /** + * Interface to synchronize {@link View} state and the screen. + * + * {@hide} + */ + public interface SurfaceSynchronizer { + /** + * Wait until requested change on a {@link View} is reflected on the screen. + * + * @param callback callback to run after the change is reflected on the screen. + */ + void syncSurfaceAndRun(Runnable callback); + } + + private static final SurfaceSynchronizer DEFAULT_SURFACE_SYNCHRONIZER = + new SurfaceSynchronizer() { + @Override + public void syncSurfaceAndRun(Runnable callback) { + Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { + // Just wait 2 frames. There is no guarantee, but this is usually enough time that + // the requested change is reflected on the screen. + // TODO: Once SurfaceFlinger provide APIs to sync the state of {@code View} and + // surfaces, rewrite this logic with them. + private int mFrameWait = 2; + + @Override + public void doFrame(long frameTimeNanos) { + if (--mFrameWait > 0) { + Choreographer.getInstance().postFrameCallback(this); + } else { + callback.run(); + } + } + }); + } + }; + private final BubbleController mBubbleController; + private final BubbleData mBubbleData; + + private final ValueAnimator mDesaturateAndDarkenAnimator; + private final Paint mDesaturateAndDarkenPaint = new Paint(); + + private PhysicsAnimationLayout mBubbleContainer; + private StackAnimationController mStackAnimationController; + private ExpandedAnimationController mExpandedAnimationController; + + private View mTaskbarScrim; + private FrameLayout mExpandedViewContainer; + + /** Matrix used to scale the expanded view container with a given pivot point. */ + private final AnimatableScaleMatrix mExpandedViewContainerMatrix = new AnimatableScaleMatrix(); + + /** + * SurfaceView that we draw screenshots of animating-out bubbles into. This allows us to animate + * between bubble activities without needing both to be alive at the same time. + */ + private SurfaceView mAnimatingOutSurfaceView; + + /** Container for the animating-out SurfaceView. */ + private FrameLayout mAnimatingOutSurfaceContainer; + + /** + * Buffer containing a screenshot of the animating-out bubble. This is drawn into the + * SurfaceView during animations. + */ + private SurfaceControl.ScreenshotHardwareBuffer mAnimatingOutBubbleBuffer; + + private BubbleFlyoutView mFlyout; + /** Runnable that fades out the flyout and then sets it to GONE. */ + private Runnable mHideFlyout = () -> animateFlyoutCollapsed(true, 0 /* velX */); + /** + * Callback to run after the flyout hides. Also called if a new flyout is shown before the + * previous one animates out. + */ + private Runnable mAfterFlyoutHidden; + /** + * Set when the flyout is tapped, so that we can expand the bubble associated with the flyout + * once it collapses. + */ + @Nullable + private BubbleViewProvider mBubbleToExpandAfterFlyoutCollapse = null; + + /** Layout change listener that moves the stack to the nearest valid position on rotation. */ + private OnLayoutChangeListener mOrientationChangedListener; + + @Nullable private RelativeStackPosition mRelativeStackPositionBeforeRotation; + + private int mMaxBubbles; + private int mBubbleSize; + private int mBubbleElevation; + private int mBubblePaddingTop; + private int mBubbleTouchPadding; + private int mExpandedViewPadding; + private int mPointerHeight; + private int mCornerRadius; + private int mImeOffset; + @Nullable private BubbleViewProvider mExpandedBubble; + private boolean mIsExpanded; + + /** Whether the stack is currently on the left side of the screen, or animating there. */ + private boolean mStackOnLeftOrWillBe = true; + + /** Whether a touch gesture, such as a stack/bubble drag or flyout drag, is in progress. */ + private boolean mIsGestureInProgress = false; + + /** Whether or not the stack is temporarily invisible off the side of the screen. */ + private boolean mTemporarilyInvisible = false; + + /** Whether we're in the middle of dragging the stack around by touch. */ + private boolean mIsDraggingStack = false; + + /** + * The pointer index of the ACTION_DOWN event we received prior to an ACTION_UP. We'll ignore + * touches from other pointer indices. + */ + private int mPointerIndexDown = -1; + + /** Description of current animation controller state. */ + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("Stack view state:"); + + String bubblesOnScreen = BubbleDebugConfig.formatBubblesString( + getBubblesOnScreen(), getExpandedBubble()); + pw.print(" bubbles on screen: "); pw.println(bubblesOnScreen); + pw.print(" gestureInProgress: "); pw.println(mIsGestureInProgress); + pw.print(" showingDismiss: "); pw.println(mDismissView.isShowing()); + pw.print(" isExpansionAnimating: "); pw.println(mIsExpansionAnimating); + pw.print(" expandedContainerVis: "); pw.println(mExpandedViewContainer.getVisibility()); + pw.print(" expandedContainerAlpha: "); pw.println(mExpandedViewContainer.getAlpha()); + pw.print(" expandedContainerMatrix: "); + pw.println(mExpandedViewContainer.getAnimationMatrix()); + + mStackAnimationController.dump(fd, pw, args); + mExpandedAnimationController.dump(fd, pw, args); + + if (mExpandedBubble != null) { + pw.println("Expanded bubble state:"); + pw.println(" expandedBubbleKey: " + mExpandedBubble.getKey()); + + final BubbleExpandedView expandedView = mExpandedBubble.getExpandedView(); + + if (expandedView != null) { + pw.println(" expandedViewVis: " + expandedView.getVisibility()); + pw.println(" expandedViewAlpha: " + expandedView.getAlpha()); + pw.println(" expandedViewTaskId: " + expandedView.getTaskId()); + + final View av = expandedView.getTaskView(); + + if (av != null) { + pw.println(" activityViewVis: " + av.getVisibility()); + pw.println(" activityViewAlpha: " + av.getAlpha()); + } else { + pw.println(" activityView is null"); + } + } else { + pw.println("Expanded bubble view state: expanded bubble view is null"); + } + } else { + pw.println("Expanded bubble state: expanded bubble is null"); + } + } + + private Bubbles.BubbleExpandListener mExpandListener; + + /** Callback to run when we want to unbubble the given notification's conversation. */ + private Consumer<String> mUnbubbleConversationCallback; + + private boolean mViewUpdatedRequested = false; + private boolean mIsExpansionAnimating = false; + private boolean mIsBubbleSwitchAnimating = false; + + /** The view to desaturate/darken when magneted to the dismiss target. */ + @Nullable private View mDesaturateAndDarkenTargetView; + + private Rect mTempRect = new Rect(); + + private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect()); + + private ViewTreeObserver.OnPreDrawListener mViewUpdater = + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + getViewTreeObserver().removeOnPreDrawListener(mViewUpdater); + updateExpandedView(); + mViewUpdatedRequested = false; + return true; + } + }; + + private ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater = + this::updateSystemGestureExcludeRects; + + /** Float property that 'drags' the flyout. */ + private final FloatPropertyCompat mFlyoutCollapseProperty = + new FloatPropertyCompat("FlyoutCollapseSpring") { + @Override + public float getValue(Object o) { + return mFlyoutDragDeltaX; + } + + @Override + public void setValue(Object o, float v) { + setFlyoutStateForDragLength(v); + } + }; + + /** SpringAnimation that springs the flyout collapsed via onFlyoutDragged. */ + private final SpringAnimation mFlyoutTransitionSpring = + new SpringAnimation(this, mFlyoutCollapseProperty); + + /** Distance the flyout has been dragged in the X axis. */ + private float mFlyoutDragDeltaX = 0f; + + /** + * Runnable that animates in the flyout. This reference is needed to cancel delayed postings. + */ + private Runnable mAnimateInFlyout; + + /** + * End listener for the flyout spring that either posts a runnable to hide the flyout, or hides + * it immediately. + */ + private final DynamicAnimation.OnAnimationEndListener mAfterFlyoutTransitionSpring = + (dynamicAnimation, b, v, v1) -> { + if (mFlyoutDragDeltaX == 0) { + mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); + } else { + mFlyout.hideFlyout(); + } + }; + + @NonNull + private final SurfaceSynchronizer mSurfaceSynchronizer; + + /** + * The currently magnetized object, which is being dragged and will be attracted to the magnetic + * dismiss target. + * + * This is either the stack itself, or an individual bubble. + */ + private MagnetizedObject<?> mMagnetizedObject; + + /** + * The MagneticTarget instance for our circular dismiss view. This is added to the + * MagnetizedObject instances for the stack and any dragged-out bubbles. + */ + private MagnetizedObject.MagneticTarget mMagneticTarget; + + /** Magnet listener that handles animating and dismissing individual dragged-out bubbles. */ + private final MagnetizedObject.MagnetListener mIndividualBubbleMagnetListener = + new MagnetizedObject.MagnetListener() { + @Override + public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { + if (mExpandedAnimationController.getDraggedOutBubble() == null) { + return; + } + + animateDesaturateAndDarken( + mExpandedAnimationController.getDraggedOutBubble(), true); + } + + @Override + public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, + float velX, float velY, boolean wasFlungOut) { + if (mExpandedAnimationController.getDraggedOutBubble() == null) { + return; + } + + animateDesaturateAndDarken( + mExpandedAnimationController.getDraggedOutBubble(), false); + + if (wasFlungOut) { + mExpandedAnimationController.snapBubbleBack( + mExpandedAnimationController.getDraggedOutBubble(), velX, velY); + mDismissView.hide(); + } else { + mExpandedAnimationController.onUnstuckFromTarget(); + } + } + + @Override + public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { + if (mExpandedAnimationController.getDraggedOutBubble() == null) { + return; + } + + mExpandedAnimationController.dismissDraggedOutBubble( + mExpandedAnimationController.getDraggedOutBubble() /* bubble */, + mDismissView.getHeight() /* translationYBy */, + BubbleStackView.this::dismissMagnetizedObject /* after */); + mDismissView.hide(); + } + }; + + /** Magnet listener that handles animating and dismissing the entire stack. */ + private final MagnetizedObject.MagnetListener mStackMagnetListener = + new MagnetizedObject.MagnetListener() { + @Override + public void onStuckToTarget( + @NonNull MagnetizedObject.MagneticTarget target) { + animateDesaturateAndDarken(mBubbleContainer, true); + } + + @Override + public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, + float velX, float velY, boolean wasFlungOut) { + animateDesaturateAndDarken(mBubbleContainer, false); + + if (wasFlungOut) { + mStackAnimationController.flingStackThenSpringToEdge( + mStackAnimationController.getStackPosition().x, velX, velY); + mDismissView.hide(); + } else { + mStackAnimationController.onUnstuckFromTarget(); + } + } + + @Override + public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { + mStackAnimationController.animateStackDismissal( + mDismissView.getHeight() /* translationYBy */, + () -> { + resetDesaturationAndDarken(); + dismissMagnetizedObject(); + } + ); + + mDismissView.hide(); + } + }; + + /** + * Click listener set on each bubble view. When collapsed, clicking a bubble expands the stack. + * When expanded, clicking a bubble either expands that bubble, or collapses the stack. + */ + private OnClickListener mBubbleClickListener = new OnClickListener() { + @Override + public void onClick(View view) { + mIsDraggingStack = false; // If the touch ended in a click, we're no longer dragging. + + // Bubble clicks either trigger expansion/collapse or a bubble switch, both of which we + // shouldn't interrupt. These are quick transitions, so it's not worth trying to adjust + // the animations inflight. + if (mIsExpansionAnimating || mIsBubbleSwitchAnimating) { + return; + } + + final Bubble clickedBubble = mBubbleData.getBubbleWithView(view); + + // If the bubble has since left us, ignore the click. + if (clickedBubble == null) { + return; + } + + final boolean clickedBubbleIsCurrentlyExpandedBubble = + clickedBubble.getKey().equals(mExpandedBubble.getKey()); + + if (isExpanded()) { + mExpandedAnimationController.onGestureFinished(); + } + + if (isExpanded() && !clickedBubbleIsCurrentlyExpandedBubble) { + if (clickedBubble != mBubbleData.getSelectedBubble()) { + // Select the clicked bubble. + mBubbleData.setSelectedBubble(clickedBubble); + } else { + // If the clicked bubble is the selected bubble (but not the expanded bubble), + // that means overflow was previously expanded. Set the selected bubble + // internally without going through BubbleData (which would ignore it since it's + // already selected). + setSelectedBubble(clickedBubble); + } + } else { + // Otherwise, we either tapped the stack (which means we're collapsed + // and should expand) or the currently selected bubble (we're expanded + // and should collapse). + if (!maybeShowStackEdu()) { + mBubbleData.setExpanded(!mBubbleData.isExpanded()); + } + } + } + }; + + /** + * Touch listener set on each bubble view. This enables dragging and dismissing the stack (when + * collapsed), or individual bubbles (when expanded). + */ + private RelativeTouchListener mBubbleTouchListener = new RelativeTouchListener() { + + @Override + public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) { + // If we're expanding or collapsing, consume but ignore all touch events. + if (mIsExpansionAnimating) { + return true; + } + + // If the manage menu is visible, just hide it. + if (mShowingManage) { + showManageMenu(false /* show */); + } + + if (mBubbleData.isExpanded()) { + if (mManageEduView != null) { + mManageEduView.hide(false /* show */); + } + + // If we're expanded, tell the animation controller to prepare to drag this bubble, + // dispatching to the individual bubble magnet listener. + mExpandedAnimationController.prepareForBubbleDrag( + v /* bubble */, + mMagneticTarget, + mIndividualBubbleMagnetListener); + + hideCurrentInputMethod(); + + // Save the magnetized individual bubble so we can dispatch touch events to it. + mMagnetizedObject = mExpandedAnimationController.getMagnetizedBubbleDraggingOut(); + } else { + // If we're collapsed, prepare to drag the stack. Cancel active animations, set the + // animation controller, and hide the flyout. + mStackAnimationController.cancelStackPositionAnimations(); + mBubbleContainer.setActiveController(mStackAnimationController); + hideFlyoutImmediate(); + + if (!mPositioner.showingInTaskbar()) { + // Also, save the magnetized stack so we can dispatch touch events to it. + mMagnetizedObject = mStackAnimationController.getMagnetizedStack( + mMagneticTarget); + mMagnetizedObject.setMagnetListener(mStackMagnetListener); + } else { + // In taskbar, the stack isn't draggable so we shouldn't dispatch touch events. + mMagnetizedObject = null; + } + + // Also, save the magnetized stack so we can dispatch touch events to it. + mMagnetizedObject = mStackAnimationController.getMagnetizedStack(mMagneticTarget); + mMagnetizedObject.setMagnetListener(mStackMagnetListener); + + mIsDraggingStack = true; + + // Cancel animations to make the stack temporarily invisible, since we're now + // dragging it. + updateTemporarilyInvisibleAnimation(false /* hideImmediately */); + } + + passEventToMagnetizedObject(ev); + + // Bubbles are always interested in all touch events! + return true; + } + + @Override + public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, + float viewInitialY, float dx, float dy) { + // If we're expanding or collapsing, ignore all touch events. + if (mIsExpansionAnimating + // Also ignore events if we shouldn't be draggable. + || (mPositioner.showingInTaskbar() && !mIsExpanded)) { + return; + } + + // Show the dismiss target, if we haven't already. + mDismissView.show(); + + // First, see if the magnetized object consumes the event - if so, we shouldn't move the + // bubble since it's stuck to the target. + if (!passEventToMagnetizedObject(ev)) { + if (mBubbleData.isExpanded() || mPositioner.showingInTaskbar()) { + mExpandedAnimationController.dragBubbleOut( + v, viewInitialX + dx, viewInitialY + dy); + } else { + if (mStackEduView != null) { + mStackEduView.hide(false /* fromExpansion */); + } + mStackAnimationController.moveStackFromTouch( + viewInitialX + dx, viewInitialY + dy); + } + } + } + + @Override + public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, + float viewInitialY, float dx, float dy, float velX, float velY) { + // If we're expanding or collapsing, ignore all touch events. + if (mIsExpansionAnimating + // Also ignore events if we shouldn't be draggable. + || (mPositioner.showingInTaskbar() && !mIsExpanded)) { + return; + } + + // First, see if the magnetized object consumes the event - if so, the bubble was + // released in the target or flung out of it, and we should ignore the event. + if (!passEventToMagnetizedObject(ev)) { + if (mBubbleData.isExpanded()) { + mExpandedAnimationController.snapBubbleBack(v, velX, velY); + } else { + // Fling the stack to the edge, and save whether or not it's going to end up on + // the left side of the screen. + final boolean oldOnLeft = mStackOnLeftOrWillBe; + mStackOnLeftOrWillBe = + mStackAnimationController.flingStackThenSpringToEdge( + viewInitialX + dx, velX, velY) <= 0; + final boolean updateForCollapsedStack = oldOnLeft != mStackOnLeftOrWillBe; + updateBadgesAndZOrder(updateForCollapsedStack); + logBubbleEvent(null /* no bubble associated with bubble stack move */, + FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED); + } + mDismissView.hide(); + } + + mIsDraggingStack = false; + + // Hide the stack after a delay, if needed. + updateTemporarilyInvisibleAnimation(false /* hideImmediately */); + } + }; + + /** Click listener set on the flyout, which expands the stack when the flyout is tapped. */ + private OnClickListener mFlyoutClickListener = new OnClickListener() { + @Override + public void onClick(View view) { + if (maybeShowStackEdu()) { + // If we're showing user education, don't open the bubble show the education first + mBubbleToExpandAfterFlyoutCollapse = null; + } else { + mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble(); + } + + mFlyout.removeCallbacks(mHideFlyout); + mHideFlyout.run(); + } + }; + + /** Touch listener for the flyout. This enables the drag-to-dismiss gesture on the flyout. */ + private RelativeTouchListener mFlyoutTouchListener = new RelativeTouchListener() { + + @Override + public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) { + mFlyout.removeCallbacks(mHideFlyout); + return true; + } + + @Override + public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, + float viewInitialY, float dx, float dy) { + setFlyoutStateForDragLength(dx); + } + + @Override + public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, + float viewInitialY, float dx, float dy, float velX, float velY) { + final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); + final boolean metRequiredVelocity = + onLeft ? velX < -FLYOUT_DISMISS_VELOCITY : velX > FLYOUT_DISMISS_VELOCITY; + final boolean metRequiredDeltaX = + onLeft + ? dx < -mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS + : dx > mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS; + final boolean isCancelFling = onLeft ? velX > 0 : velX < 0; + final boolean shouldDismiss = metRequiredVelocity + || (metRequiredDeltaX && !isCancelFling); + + mFlyout.removeCallbacks(mHideFlyout); + animateFlyoutCollapsed(shouldDismiss, velX); + + maybeShowStackEdu(); + } + }; + + private BubbleOverflow mBubbleOverflow; + private StackEducationView mStackEduView; + private ManageEducationView mManageEduView; + private DismissView mDismissView; + + private ViewGroup mManageMenu; + private ImageView mManageSettingsIcon; + private TextView mManageSettingsText; + private boolean mShowingManage = false; + private PhysicsAnimator.SpringConfig mManageSpringConfig = new PhysicsAnimator.SpringConfig( + SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY); + private BubblePositioner mPositioner; + + @SuppressLint("ClickableViewAccessibility") + public BubbleStackView(Context context, BubbleController bubbleController, + BubbleData data, @Nullable SurfaceSynchronizer synchronizer, + FloatingContentCoordinator floatingContentCoordinator, + ShellExecutor mainExecutor) { + super(context); + + mDelayedAnimationExecutor = mainExecutor; + mBubbleController = bubbleController; + mBubbleData = data; + + Resources res = getResources(); + mMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered); + mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size); + mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); + mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); + mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding); + mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height); + mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset); + + mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); + int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); + + mPositioner = mBubbleController.getPositioner(); + + final TypedArray ta = mContext.obtainStyledAttributes( + new int[] {android.R.attr.dialogCornerRadius}); + mCornerRadius = ta.getDimensionPixelSize(0, 0); + ta.recycle(); + + final Runnable onBubbleAnimatedOut = () -> { + if (getBubbleCount() == 0 && !mBubbleData.isShowingOverflow()) { + mBubbleController.onAllBubblesAnimatedOut(); + } + }; + + mStackAnimationController = new StackAnimationController( + floatingContentCoordinator, this::getBubbleCount, onBubbleAnimatedOut, mPositioner); + + mExpandedAnimationController = new ExpandedAnimationController( + mPositioner, mExpandedViewPadding, onBubbleAnimatedOut); + mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER; + + // Force LTR by default since most of the Bubbles UI is positioned manually by the user, or + // is centered. It greatly simplifies translation positioning/animations. Views that will + // actually lay out differently in RTL, such as the flyout and expanded view, will set their + // layout direction to LOCALE. + setLayoutDirection(LAYOUT_DIRECTION_LTR); + + mBubbleContainer = new PhysicsAnimationLayout(context); + mBubbleContainer.setActiveController(mStackAnimationController); + mBubbleContainer.setElevation(elevation); + mBubbleContainer.setClipChildren(false); + addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + + updateUserEdu(); + + mExpandedViewContainer = new FrameLayout(context); + mExpandedViewContainer.setElevation(elevation); + mExpandedViewContainer.setClipChildren(false); + addView(mExpandedViewContainer); + + mAnimatingOutSurfaceContainer = new FrameLayout(getContext()); + mAnimatingOutSurfaceContainer.setLayoutParams( + new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + addView(mAnimatingOutSurfaceContainer); + + mAnimatingOutSurfaceView = new SurfaceView(getContext()); + mAnimatingOutSurfaceView.setUseAlpha(); + mAnimatingOutSurfaceView.setZOrderOnTop(true); + mAnimatingOutSurfaceView.setCornerRadius(mCornerRadius); + mAnimatingOutSurfaceView.setLayoutParams(new ViewGroup.LayoutParams(0, 0)); + mAnimatingOutSurfaceContainer.addView(mAnimatingOutSurfaceView); + + mAnimatingOutSurfaceContainer.setPadding( + mExpandedViewContainer.getPaddingLeft(), + mExpandedViewContainer.getPaddingTop(), + mExpandedViewContainer.getPaddingRight(), + mExpandedViewContainer.getPaddingBottom()); + + setUpManageMenu(); + + setUpFlyout(); + mFlyoutTransitionSpring.setSpring(new SpringForce() + .setStiffness(SpringForce.STIFFNESS_LOW) + .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)); + mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring); + + mDismissView = new DismissView(context); + addView(mDismissView); + + final ContentResolver contentResolver = getContext().getContentResolver(); + final int dismissRadius = Settings.Secure.getInt( + contentResolver, "bubble_dismiss_radius", mBubbleSize * 2 /* default */); + + // Save the MagneticTarget instance for the newly set up view - we'll add this to the + // MagnetizedObjects. + mMagneticTarget = new MagnetizedObject.MagneticTarget( + mDismissView.getCircle(), dismissRadius); + + setClipChildren(false); + setFocusable(true); + mBubbleContainer.bringToFront(); + + mBubbleOverflow = mBubbleData.getOverflow(); + mBubbleContainer.addView(mBubbleOverflow.getIconView(), + mBubbleContainer.getChildCount() /* index */, + new FrameLayout.LayoutParams(mPositioner.getBubbleSize(), + mPositioner.getBubbleSize())); + updateOverflow(); + mBubbleOverflow.getIconView().setOnClickListener((View v) -> { + mBubbleData.setShowingOverflow(true); + mBubbleData.setSelectedBubble(mBubbleOverflow); + mBubbleData.setExpanded(true); + }); + + mTaskbarScrim = new View(getContext()); + mTaskbarScrim.setBackgroundColor(Color.BLACK); + addView(mTaskbarScrim); + mTaskbarScrim.setAlpha(0f); + mTaskbarScrim.setVisibility(GONE); + + setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> { + mBubbleController.onImeVisibilityChanged( + insets.getInsets(WindowInsets.Type.ime()).bottom > 0); + if (!mIsExpanded || mIsExpansionAnimating) { + return view.onApplyWindowInsets(insets); + } + return view.onApplyWindowInsets(insets); + }); + + mOrientationChangedListener = + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + onDisplaySizeChanged(); + mExpandedAnimationController.updateResources(); + mStackAnimationController.updateResources(); + mBubbleOverflow.updateResources(); + + if (mRelativeStackPositionBeforeRotation != null) { + mStackAnimationController.setStackPosition( + mRelativeStackPositionBeforeRotation); + mRelativeStackPositionBeforeRotation = null; + } + + if (mIsExpanded) { + // Re-draw bubble row and pointer for new orientation. + beforeExpandedViewAnimation(); + updateOverflowVisibility(); + updatePointerPosition(); + mExpandedAnimationController.expandFromStack(() -> { + afterExpandedViewAnimation(); + } /* after */); + mExpandedViewContainer.setTranslationX(0f); + mExpandedViewContainer.setTranslationY(getExpandedViewY()); + mExpandedViewContainer.setAlpha(1f); + } + removeOnLayoutChangeListener(mOrientationChangedListener); + }; + + // This must be a separate OnDrawListener since it should be called for every draw. + getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater); + + final ColorMatrix animatedMatrix = new ColorMatrix(); + final ColorMatrix darkenMatrix = new ColorMatrix(); + + mDesaturateAndDarkenAnimator = ValueAnimator.ofFloat(1f, 0f); + mDesaturateAndDarkenAnimator.addUpdateListener(animation -> { + final float animatedValue = (float) animation.getAnimatedValue(); + animatedMatrix.setSaturation(animatedValue); + + final float animatedDarkenValue = (1f - animatedValue) * DARKEN_PERCENT; + darkenMatrix.setScale( + 1f - animatedDarkenValue /* red */, + 1f - animatedDarkenValue /* green */, + 1f - animatedDarkenValue /* blue */, + 1f /* alpha */); + + // Concat the matrices so that the animatedMatrix both desaturates and darkens. + animatedMatrix.postConcat(darkenMatrix); + + // Update the paint and apply it to the bubble container. + mDesaturateAndDarkenPaint.setColorFilter(new ColorMatrixColorFilter(animatedMatrix)); + + if (mDesaturateAndDarkenTargetView != null) { + mDesaturateAndDarkenTargetView.setLayerPaint(mDesaturateAndDarkenPaint); + } + }); + + // If the stack itself is touched, it means none of its touchable views (bubbles, flyouts, + // ActivityViews, etc.) were touched. Collapse the stack if it's expanded. + setOnTouchListener((view, ev) -> { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + if (mShowingManage) { + showManageMenu(false /* show */); + } else if (mBubbleData.isExpanded()) { + mBubbleData.setExpanded(false); + } + } + + return true; + }); + + animate() + .setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED) + .setDuration(FADE_IN_DURATION); + } + + /** + * Sets whether or not the stack should become temporarily invisible by moving off the side of + * the screen. + * + * If a flyout comes in while it's invisible, it will animate back in while the flyout is + * showing but disappear again when the flyout is gone. + */ + public void setTemporarilyInvisible(boolean invisible) { + mTemporarilyInvisible = invisible; + + // If we are animating out, hide immediately if possible so we animate out with the status + // bar. + updateTemporarilyInvisibleAnimation(invisible /* hideImmediately */); + } + + /** + * Animates the stack to be temporarily invisible, if needed. + * + * If we're currently dragging the stack, or a flyout is visible, the stack will remain visible. + * regardless of the value of {@link #mTemporarilyInvisible}. This method is called on ACTION_UP + * as well as whenever a flyout hides, so we will animate invisible at that point if needed. + */ + private void updateTemporarilyInvisibleAnimation(boolean hideImmediately) { + removeCallbacks(mAnimateTemporarilyInvisibleImmediate); + + if (mIsDraggingStack) { + // If we're dragging the stack, don't animate it invisible. + return; + } + + final boolean shouldHide = + mTemporarilyInvisible && mFlyout.getVisibility() != View.VISIBLE; + + postDelayed(mAnimateTemporarilyInvisibleImmediate, + shouldHide && !hideImmediately ? ANIMATE_TEMPORARILY_INVISIBLE_DELAY : 0); + } + + private final Runnable mAnimateTemporarilyInvisibleImmediate = () -> { + if (mTemporarilyInvisible && mFlyout.getVisibility() != View.VISIBLE) { + if (mStackAnimationController.isStackOnLeftSide()) { + animate().translationX(-mBubbleSize).start(); + } else { + animate().translationX(mBubbleSize).start(); + } + } else { + animate().translationX(0).start(); + } + }; + + private void setUpManageMenu() { + if (mManageMenu != null) { + removeView(mManageMenu); + } + + mManageMenu = (ViewGroup) LayoutInflater.from(getContext()).inflate( + R.layout.bubble_manage_menu, this, false); + mManageMenu.setVisibility(View.INVISIBLE); + + PhysicsAnimator.getInstance(mManageMenu).setDefaultSpringConfig(mManageSpringConfig); + + mManageMenu.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius); + } + }); + mManageMenu.setClipToOutline(true); + + mManageMenu.findViewById(R.id.bubble_manage_menu_dismiss_container).setOnClickListener( + view -> { + showManageMenu(false /* show */); + dismissBubbleIfExists(mBubbleData.getSelectedBubble()); + }); + + mManageMenu.findViewById(R.id.bubble_manage_menu_dont_bubble_container).setOnClickListener( + view -> { + showManageMenu(false /* show */); + mUnbubbleConversationCallback.accept(mBubbleData.getSelectedBubble().getKey()); + }); + + mManageMenu.findViewById(R.id.bubble_manage_menu_settings_container).setOnClickListener( + view -> { + showManageMenu(false /* show */); + final BubbleViewProvider bubble = mBubbleData.getSelectedBubble(); + if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { + // If it's in the stack it's a proper Bubble. + final Intent intent = ((Bubble) bubble).getSettingsIntent(mContext); + mBubbleData.setExpanded(false); + mContext.startActivityAsUser(intent, ((Bubble) bubble).getUser()); + logBubbleEvent(bubble, + FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS); + } + }); + + mManageSettingsIcon = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_icon); + mManageSettingsText = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_name); + + // The menu itself should respect locale direction so the icons are on the correct side. + mManageMenu.setLayoutDirection(LAYOUT_DIRECTION_LOCALE); + addView(mManageMenu); + } + + /** + * Whether the educational view should show for the expanded view "manage" menu. + */ + private boolean shouldShowManageEdu() { + final boolean seen = getPrefBoolean(ManageEducationViewKt.PREF_MANAGED_EDUCATION); + final boolean shouldShow = (!seen || BubbleDebugConfig.forceShowUserEducation(mContext)) + && mExpandedBubble != null; + if (BubbleDebugConfig.DEBUG_USER_EDUCATION) { + Log.d(TAG, "Show manage edu: " + shouldShow); + } + return shouldShow; + } + + private void maybeShowManageEdu() { + if (!shouldShowManageEdu()) { + return; + } + if (mManageEduView == null) { + mManageEduView = new ManageEducationView(mContext); + addView(mManageEduView); + } + mManageEduView.show(mExpandedBubble.getExpandedView(), mTempRect); + } + + /** + * Whether education view should show for the collapsed stack. + */ + private boolean shouldShowStackEdu() { + final boolean seen = getPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION); + final boolean shouldShow = !seen || BubbleDebugConfig.forceShowUserEducation(mContext); + if (BubbleDebugConfig.DEBUG_USER_EDUCATION) { + Log.d(TAG, "Show stack edu: " + shouldShow); + } + return shouldShow; + } + + private boolean getPrefBoolean(String key) { + return mContext.getSharedPreferences(mContext.getPackageName(), Context.MODE_PRIVATE) + .getBoolean(key, false /* default */); + } + + /** + * @return true if education view for collapsed stack should show and was not showing before. + */ + private boolean maybeShowStackEdu() { + if (!shouldShowStackEdu()) { + return false; + } + if (mStackEduView == null) { + mStackEduView = new StackEducationView(mContext); + addView(mStackEduView); + } + return mStackEduView.show(mPositioner.getDefaultStartPosition()); + } + + private void updateUserEdu() { + maybeShowStackEdu(); + if (mManageEduView != null) { + mManageEduView.invalidate(); + } + maybeShowManageEdu(); + if (mStackEduView != null) { + mStackEduView.invalidate(); + } + } + + @SuppressLint("ClickableViewAccessibility") + private void setUpFlyout() { + if (mFlyout != null) { + removeView(mFlyout); + } + mFlyout = new BubbleFlyoutView(getContext()); + mFlyout.setVisibility(GONE); + mFlyout.setOnClickListener(mFlyoutClickListener); + mFlyout.setOnTouchListener(mFlyoutTouchListener); + addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + } + + void updateFontScale(float fontScale) { + mFlyout.updateFontSize(fontScale); + } + + private void updateOverflow() { + mBubbleOverflow.update(); + mBubbleContainer.reorderView(mBubbleOverflow.getIconView(), + mBubbleContainer.getChildCount() - 1 /* index */); + updateOverflowVisibility(); + } + + void updateOverflowButtonDot() { + for (Bubble b : mBubbleData.getOverflowBubbles()) { + if (b.showDot()) { + mBubbleOverflow.setShowDot(true); + return; + } + } + mBubbleOverflow.setShowDot(false); + } + + /** + * Handle theme changes. + */ + public void onThemeChanged() { + setUpFlyout(); + setUpManageMenu(); + updateOverflow(); + updateUserEdu(); + updateExpandedViewTheme(); + } + + /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */ + public void onOrientationChanged() { + Resources res = getContext().getResources(); + mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); + + mRelativeStackPositionBeforeRotation = new RelativeStackPosition( + mPositioner.getRestingPosition(), + mStackAnimationController.getAllowableStackPositionRegion()); + mManageMenu.setVisibility(View.INVISIBLE); + mShowingManage = false; + + addOnLayoutChangeListener(mOrientationChangedListener); + hideFlyoutImmediate(); + } + + /** Tells the views with locale-dependent layout direction to resolve the new direction. */ + public void onLayoutDirectionChanged(int direction) { + mManageMenu.setLayoutDirection(direction); + mFlyout.setLayoutDirection(direction); + if (mStackEduView != null) { + mStackEduView.setLayoutDirection(direction); + } + if (mManageEduView != null) { + mManageEduView.setLayoutDirection(direction); + } + updateExpandedViewDirection(direction); + } + + /** Respond to the display size change by recalculating view size and location. */ + public void onDisplaySizeChanged() { + updateOverflow(); + + Resources res = getContext().getResources(); + mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); + mBubbleSize = mPositioner.getBubbleSize(); + for (Bubble b : mBubbleData.getBubbles()) { + if (b.getIconView() == null) { + Log.d(TAG, "Display size changed. Icon null: " + b); + continue; + } + b.getIconView().setLayoutParams(new LayoutParams(mBubbleSize, mBubbleSize)); + } + mBubbleOverflow.getIconView().setLayoutParams(new LayoutParams(mBubbleSize, mBubbleSize)); + mExpandedAnimationController.updateResources(); + mStackAnimationController.updateResources(); + mDismissView.updateResources(); + mMagneticTarget.setMagneticFieldRadiusPx(mBubbleSize * 2); + } + + @Override + public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { + inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + + mTempRect.setEmpty(); + getTouchableRegion(mTempRect); + inoutInfo.touchableRegion.set(mTempRect); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + getViewTreeObserver().addOnComputeInternalInsetsListener(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + getViewTreeObserver().removeOnPreDrawListener(mViewUpdater); + getViewTreeObserver().removeOnComputeInternalInsetsListener(this); + if (mBubbleOverflow != null) { + mBubbleOverflow.cleanUpExpandedState(); + } + } + + @Override + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); + setupLocalMenu(info); + } + + void updateExpandedViewTheme() { + final List<Bubble> bubbles = mBubbleData.getBubbles(); + if (bubbles.isEmpty()) { + return; + } + bubbles.forEach(bubble -> { + if (bubble.getExpandedView() != null) { + bubble.getExpandedView().applyThemeAttrs(); + } + }); + } + + void updateExpandedViewDirection(int direction) { + final List<Bubble> bubbles = mBubbleData.getBubbles(); + if (bubbles.isEmpty()) { + return; + } + bubbles.forEach(bubble -> { + if (bubble.getExpandedView() != null) { + bubble.getExpandedView().setLayoutDirection(direction); + } + }); + } + + void setupLocalMenu(AccessibilityNodeInfo info) { + Resources res = mContext.getResources(); + + // Custom local actions. + AccessibilityAction moveTopLeft = new AccessibilityAction(R.id.action_move_top_left, + res.getString(R.string.bubble_accessibility_action_move_top_left)); + info.addAction(moveTopLeft); + + AccessibilityAction moveTopRight = new AccessibilityAction(R.id.action_move_top_right, + res.getString(R.string.bubble_accessibility_action_move_top_right)); + info.addAction(moveTopRight); + + AccessibilityAction moveBottomLeft = new AccessibilityAction(R.id.action_move_bottom_left, + res.getString(R.string.bubble_accessibility_action_move_bottom_left)); + info.addAction(moveBottomLeft); + + AccessibilityAction moveBottomRight = new AccessibilityAction(R.id.action_move_bottom_right, + res.getString(R.string.bubble_accessibility_action_move_bottom_right)); + info.addAction(moveBottomRight); + + // Default actions. + info.addAction(AccessibilityAction.ACTION_DISMISS); + if (mIsExpanded) { + info.addAction(AccessibilityAction.ACTION_COLLAPSE); + } else { + info.addAction(AccessibilityAction.ACTION_EXPAND); + } + } + + @Override + public boolean performAccessibilityActionInternal(int action, Bundle arguments) { + if (super.performAccessibilityActionInternal(action, arguments)) { + return true; + } + final RectF stackBounds = mStackAnimationController.getAllowableStackPositionRegion(); + + // R constants are not final so we cannot use switch-case here. + if (action == AccessibilityNodeInfo.ACTION_DISMISS) { + mBubbleData.dismissAll(Bubbles.DISMISS_ACCESSIBILITY_ACTION); + announceForAccessibility( + getResources().getString(R.string.accessibility_bubble_dismissed)); + return true; + } else if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) { + mBubbleData.setExpanded(false); + return true; + } else if (action == AccessibilityNodeInfo.ACTION_EXPAND) { + mBubbleData.setExpanded(true); + return true; + } else if (action == R.id.action_move_top_left) { + mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.top); + return true; + } else if (action == R.id.action_move_top_right) { + mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.top); + return true; + } else if (action == R.id.action_move_bottom_left) { + mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.bottom); + return true; + } else if (action == R.id.action_move_bottom_right) { + mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.bottom); + return true; + } + return false; + } + + /** + * Update content description for a11y TalkBack. + */ + public void updateContentDescription() { + if (mBubbleData.getBubbles().isEmpty()) { + return; + } + + for (int i = 0; i < mBubbleData.getBubbles().size(); i++) { + final Bubble bubble = mBubbleData.getBubbles().get(i); + final String appName = bubble.getAppName(); + + String titleStr = bubble.getTitle(); + if (titleStr == null) { + titleStr = getResources().getString(R.string.notification_bubble_title); + } + + if (bubble.getIconView() != null) { + if (mIsExpanded || i > 0) { + bubble.getIconView().setContentDescription(getResources().getString( + R.string.bubble_content_description_single, titleStr, appName)); + } else { + final int moreCount = mBubbleContainer.getChildCount() - 1; + bubble.getIconView().setContentDescription(getResources().getString( + R.string.bubble_content_description_stack, + titleStr, appName, moreCount)); + } + } + } + } + + private void updateSystemGestureExcludeRects() { + // Exclude the region occupied by the first BubbleView in the stack + Rect excludeZone = mSystemGestureExclusionRects.get(0); + if (getBubbleCount() > 0) { + View firstBubble = mBubbleContainer.getChildAt(0); + excludeZone.set(firstBubble.getLeft(), firstBubble.getTop(), firstBubble.getRight(), + firstBubble.getBottom()); + excludeZone.offset((int) (firstBubble.getTranslationX() + 0.5f), + (int) (firstBubble.getTranslationY() + 0.5f)); + mBubbleContainer.setSystemGestureExclusionRects(mSystemGestureExclusionRects); + } else { + excludeZone.setEmpty(); + mBubbleContainer.setSystemGestureExclusionRects(Collections.emptyList()); + } + } + + /** + * Sets the listener to notify when the bubble stack is expanded. + */ + public void setExpandListener(Bubbles.BubbleExpandListener listener) { + mExpandListener = listener; + } + + /** Sets the function to call to un-bubble the given conversation. */ + public void setUnbubbleConversationCallback( + Consumer<String> unbubbleConversationCallback) { + mUnbubbleConversationCallback = unbubbleConversationCallback; + } + + /** + * Whether the stack of bubbles is expanded or not. + */ + public boolean isExpanded() { + return mIsExpanded; + } + + /** + * Whether the stack of bubbles is animating to or from expansion. + */ + public boolean isExpansionAnimating() { + return mIsExpansionAnimating; + } + + /** + * The {@link Bubble} that is expanded, null if one does not exist. + */ + @VisibleForTesting + @Nullable + public BubbleViewProvider getExpandedBubble() { + return mExpandedBubble; + } + + // via BubbleData.Listener + @SuppressLint("ClickableViewAccessibility") + void addBubble(Bubble bubble) { + if (DEBUG_BUBBLE_STACK_VIEW) { + Log.d(TAG, "addBubble: " + bubble); + } + + if (getBubbleCount() == 0 && shouldShowStackEdu()) { + // Override the default stack position if we're showing user education. + mStackAnimationController.setStackPosition(mPositioner.getDefaultStartPosition()); + } + + if (getBubbleCount() == 0) { + mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); + } + + if (bubble.getIconView() == null) { + return; + } + + // Set the dot position to the opposite of the side the stack is resting on, since the stack + // resting slightly off-screen would result in the dot also being off-screen. + bubble.getIconView().setDotBadgeOnLeft(!mStackOnLeftOrWillBe /* onLeft */); + + bubble.getIconView().setOnClickListener(mBubbleClickListener); + bubble.getIconView().setOnTouchListener(mBubbleTouchListener); + + mBubbleContainer.addView(bubble.getIconView(), 0, + new FrameLayout.LayoutParams(mPositioner.getBubbleSize(), + mPositioner.getBubbleSize())); + animateInFlyoutForBubble(bubble); + requestUpdate(); + logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__POSTED); + } + + // via BubbleData.Listener + void removeBubble(Bubble bubble) { + if (DEBUG_BUBBLE_STACK_VIEW) { + Log.d(TAG, "removeBubble: " + bubble); + } + // Remove it from the views + for (int i = 0; i < getBubbleCount(); i++) { + View v = mBubbleContainer.getChildAt(i); + if (v instanceof BadgedImageView + && ((BadgedImageView) v).getKey().equals(bubble.getKey())) { + mBubbleContainer.removeViewAt(i); + if (mBubbleData.hasOverflowBubbleWithKey(bubble.getKey())) { + bubble.cleanupExpandedView(); + } else { + bubble.cleanupViews(); + } + updatePointerPosition(); + logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); + return; + } + } + Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble); + } + + private void updateOverflowVisibility() { + mBubbleOverflow.setVisible((mIsExpanded || mBubbleData.isShowingOverflow()) + ? VISIBLE + : GONE); + } + + // via BubbleData.Listener + void updateBubble(Bubble bubble) { + animateInFlyoutForBubble(bubble); + requestUpdate(); + logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__UPDATED); + } + + /** + * Update bubble order and pointer position. + */ + public void updateBubbleOrder(List<Bubble> bubbles) { + final Runnable reorder = () -> { + for (int i = 0; i < bubbles.size(); i++) { + Bubble bubble = bubbles.get(i); + mBubbleContainer.reorderView(bubble.getIconView(), i); + } + }; + if (mIsExpanded) { + reorder.run(); + updateBadgesAndZOrder(false /* setBadgeForCollapsedStack */); + } else { + List<View> bubbleViews = bubbles.stream() + .map(b -> b.getIconView()).collect(Collectors.toList()); + mStackAnimationController.animateReorder(bubbleViews, reorder); + } + updatePointerPosition(); + } + + /** + * Changes the currently selected bubble. If the stack is already expanded, the newly selected + * bubble will be shown immediately. This does not change the expanded state or change the + * position of any bubble. + */ + // via BubbleData.Listener + public void setSelectedBubble(@Nullable BubbleViewProvider bubbleToSelect) { + if (DEBUG_BUBBLE_STACK_VIEW) { + Log.d(TAG, "setSelectedBubble: " + bubbleToSelect); + } + + if (bubbleToSelect == null) { + mBubbleData.setShowingOverflow(false); + return; + } + + // Ignore this new bubble only if it is the exact same bubble object. Otherwise, we'll want + // to re-render it even if it has the same key (equals() returns true). If the currently + // expanded bubble is removed and instantly re-added, we'll get back a new Bubble instance + // with the same key (with newly inflated expanded views), and we need to render those new + // views. + if (mExpandedBubble == bubbleToSelect) { + return; + } + + if (bubbleToSelect.getKey().equals(BubbleOverflow.KEY)) { + mBubbleData.setShowingOverflow(true); + } else { + mBubbleData.setShowingOverflow(false); + } + + if (mIsExpanded && mIsExpansionAnimating) { + // If the bubble selection changed during the expansion animation, the expanding bubble + // probably crashed or immediately removed itself (or, we just got unlucky with a new + // auto-expanding bubble showing up at just the right time). Cancel the animations so we + // can start fresh. + cancelAllExpandCollapseSwitchAnimations(); + } + + // If we're expanded, screenshot the currently expanded bubble (before expanding the newly + // selected bubble) so we can animate it out. + if (mIsExpanded && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + // Before screenshotting, have the real ActivityView show on top of other surfaces + // so that the screenshot doesn't flicker on top of it. + mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(true); + } + + try { + screenshotAnimatingOutBubbleIntoSurface((success) -> { + mAnimatingOutSurfaceContainer.setVisibility( + success ? View.VISIBLE : View.INVISIBLE); + showNewlySelectedBubble(bubbleToSelect); + }); + } catch (Exception e) { + showNewlySelectedBubble(bubbleToSelect); + e.printStackTrace(); + } + } else { + showNewlySelectedBubble(bubbleToSelect); + } + } + + private void showNewlySelectedBubble(BubbleViewProvider bubbleToSelect) { + final BubbleViewProvider previouslySelected = mExpandedBubble; + mExpandedBubble = bubbleToSelect; + updatePointerPosition(); + + if (mIsExpanded) { + hideCurrentInputMethod(); + + // Make the container of the expanded view transparent before removing the expanded view + // from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the + // expanded view becomes visible on the screen. See b/126856255 + mExpandedViewContainer.setAlpha(0.0f); + mSurfaceSynchronizer.syncSurfaceAndRun(() -> { + if (previouslySelected != null) { + previouslySelected.setContentVisibility(false); + } + + updateExpandedBubble(); + requestUpdate(); + + logBubbleEvent(previouslySelected, + FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED); + logBubbleEvent(bubbleToSelect, + FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); + notifyExpansionChanged(previouslySelected, false /* expanded */); + notifyExpansionChanged(bubbleToSelect, true /* expanded */); + }); + } + } + + /** + * Changes the expanded state of the stack. + * + * @param shouldExpand whether the bubble stack should appear expanded + */ + // via BubbleData.Listener + public void setExpanded(boolean shouldExpand) { + if (DEBUG_BUBBLE_STACK_VIEW) { + Log.d(TAG, "setExpanded: " + shouldExpand); + } + + if (!shouldExpand) { + // If we're collapsing, release the animating-out surface immediately since we have no + // need for it, and this ensures it cannot remain visible as we collapse. + releaseAnimatingOutBubbleBuffer(); + } + + if (shouldExpand == mIsExpanded) { + return; + } + + hideCurrentInputMethod(); + + mBubbleController.getSysuiProxy().onStackExpandChanged(shouldExpand); + + if (mIsExpanded) { + animateCollapse(); + logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED); + } else { + animateExpansion(); + // TODO: move next line to BubbleData + logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); + logBubbleEvent(mExpandedBubble, + FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED); + } + notifyExpansionChanged(mExpandedBubble, mIsExpanded); + } + + /** + * Asks the BubbleController to hide the IME from anywhere, whether it's focused on Bubbles or + * not. + */ + void hideCurrentInputMethod() { + mBubbleController.hideCurrentInputMethod(); + } + + /** Set the stack position to whatever the positioner says. */ + void updateStackPosition() { + mStackAnimationController.setStackPosition(mPositioner.getRestingPosition()); + mDismissView.hide(); + } + + private void beforeExpandedViewAnimation() { + mIsExpansionAnimating = true; + hideFlyoutImmediate(); + updateExpandedBubble(); + updateExpandedView(); + } + + private void afterExpandedViewAnimation() { + mIsExpansionAnimating = false; + updateExpandedView(); + requestUpdate(); + } + + private void animateExpansion() { + cancelDelayedExpandCollapseSwitchAnimations(); + final boolean showVertically = mPositioner.showBubblesVertically(); + mIsExpanded = true; + if (mStackEduView != null) { + mStackEduView.hide(true /* fromExpansion */); + } + beforeExpandedViewAnimation(); + + mBubbleContainer.setActiveController(mExpandedAnimationController); + updateOverflowVisibility(); + updatePointerPosition(); + mExpandedAnimationController.expandFromStack(() -> { + if (mIsExpanded && mExpandedBubble.getExpandedView() != null) { + maybeShowManageEdu(); + } + } /* after */); + + if (mPositioner.showingInTaskbar() + // Don't need the scrim when the bar is at the bottom + && mPositioner.getTaskbarPosition() != BubblePositioner.TASKBAR_POSITION_BOTTOM) { + mTaskbarScrim.getLayoutParams().width = mPositioner.getTaskbarSize(); + mTaskbarScrim.setTranslationX(mStackOnLeftOrWillBe + ? 0f + : mPositioner.getAvailableRect().right - mPositioner.getTaskbarSize()); + mTaskbarScrim.setVisibility(VISIBLE); + mTaskbarScrim.animate().alpha(1f).start(); + } + + mExpandedViewContainer.setTranslationX(0f); + mExpandedViewContainer.setTranslationY(getExpandedViewY()); + mExpandedViewContainer.setAlpha(1f); + + int index; + if (mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey())) { + index = mBubbleData.getBubbles().size(); + } else { + index = getBubbleIndex(mExpandedBubble); + } + // Position of the bubble we're expanding, once it's settled in its row. + final float bubbleWillBeAt = + mExpandedAnimationController.getBubbleXOrYForOrientation(index); + + // How far horizontally the bubble will be animating. We'll wait a bit longer for bubbles + // that are animating farther, so that the expanded view doesn't move as much. + final float relevantStackPosition = showVertically + ? mStackAnimationController.getStackPosition().y + : mStackAnimationController.getStackPosition().x; + final float distanceAnimated = Math.abs(bubbleWillBeAt - relevantStackPosition); + + // Wait for the path animation target to reach its end, and add a small amount of extra time + // if the bubble is moving a lot horizontally. + long startDelay = 0L; + + // Should not happen since we lay out before expanding, but just in case... + if (getWidth() > 0) { + startDelay = (long) + (ExpandedAnimationController.EXPAND_COLLAPSE_TARGET_ANIM_DURATION + + (distanceAnimated / getWidth()) * 30); + } + + // Set the pivot point for the scale, so the expanded view animates out from the bubble. + if (showVertically) { + float pivotX; + float pivotY = bubbleWillBeAt + mBubbleSize / 2f; + if (mStackOnLeftOrWillBe) { + pivotX = mPositioner.getAvailableRect().left + mBubbleSize + mExpandedViewPadding; + } else { + pivotX = mPositioner.getAvailableRect().right - mBubbleSize - mExpandedViewPadding; + } + mExpandedViewContainerMatrix.setScale( + 0f, 0f, + pivotX, pivotY); + } else { + mExpandedViewContainerMatrix.setScale( + 0f, 0f, + bubbleWillBeAt + mBubbleSize / 2f, getExpandedViewY()); + } + mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); + + if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false); + } + + mDelayedAnimationExecutor.executeDelayed(() -> { + PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); + PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) + .spring(AnimatableScaleMatrix.SCALE_X, + AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), + mScaleInSpringConfig) + .spring(AnimatableScaleMatrix.SCALE_Y, + AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), + mScaleInSpringConfig) + .addUpdateListener((target, values) -> { + if (mExpandedBubble == null || mExpandedBubble.getIconView() == null) { + return; + } + float translation = showVertically + ? mExpandedBubble.getIconView().getTranslationY() + : mExpandedBubble.getIconView().getTranslationX(); + mExpandedViewContainerMatrix.postTranslate( + translation - bubbleWillBeAt, + 0); + mExpandedViewContainer.setAnimationMatrix( + mExpandedViewContainerMatrix); + }) + .withEndActions(() -> { + afterExpandedViewAnimation(); + if (mExpandedBubble != null + && mExpandedBubble.getExpandedView() != null) { + mExpandedBubble.getExpandedView() + .setSurfaceZOrderedOnTop(false); + } + }) + .start(); + }, startDelay); + } + + private void animateCollapse() { + cancelDelayedExpandCollapseSwitchAnimations(); + + // Hide the menu if it's visible. + showManageMenu(false); + + mIsExpanded = false; + mIsExpansionAnimating = true; + + mBubbleContainer.cancelAllAnimations(); + + // If we were in the middle of swapping, the animating-out surface would have been scaling + // to zero - finish it off. + PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel(); + mAnimatingOutSurfaceContainer.setScaleX(0f); + mAnimatingOutSurfaceContainer.setScaleY(0f); + + // Let the expanded animation controller know that it shouldn't animate child adds/reorders + // since we're about to animate collapsed. + mExpandedAnimationController.notifyPreparingToCollapse(); + + final long startDelay = + (long) (ExpandedAnimationController.EXPAND_COLLAPSE_TARGET_ANIM_DURATION * 0.6f); + mDelayedAnimationExecutor.executeDelayed(() -> { + mExpandedAnimationController.collapseBackToStack( + mStackAnimationController.getStackPositionAlongNearestHorizontalEdge() + /* collapseTo */, + () -> mBubbleContainer.setActiveController(mStackAnimationController)); + }, startDelay); + + if (mTaskbarScrim.getVisibility() == VISIBLE) { + mTaskbarScrim.animate().alpha(0f).start(); + } + + // We want to visually collapse into this bubble during the animation. + final View expandingFromBubble = mExpandedBubble.getIconView(); + + int index; + if (mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey())) { + index = mBubbleData.getBubbles().size(); + } else { + index = mBubbleData.getBubbles().indexOf(mExpandedBubble); + } + // Value the bubble is animating from (back into the stack). + final float expandingFromBubbleAt = + mExpandedAnimationController.getBubbleXOrYForOrientation(index); + final boolean showVertically = mPositioner.showBubblesVertically(); + if (mPositioner.showBubblesVertically()) { + float pivotX; + float pivotY = expandingFromBubbleAt + mBubbleSize / 2f; + if (mStackOnLeftOrWillBe) { + pivotX = mPositioner.getAvailableRect().left + mBubbleSize + mExpandedViewPadding; + } else { + pivotX = mPositioner.getAvailableRect().right - mBubbleSize - mExpandedViewPadding; + } + mExpandedViewContainerMatrix.setScale( + 1f, 1f, + pivotX, pivotY); + } else { + mExpandedViewContainerMatrix.setScale( + 1f, 1f, + expandingFromBubbleAt + mBubbleSize / 2f, + getExpandedViewY()); + } + + PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); + PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) + .spring(AnimatableScaleMatrix.SCALE_X, 0f, mScaleOutSpringConfig) + .spring(AnimatableScaleMatrix.SCALE_Y, 0f, mScaleOutSpringConfig) + .addUpdateListener((target, values) -> { + if (expandingFromBubble != null) { + // Follow the bubble as it translates! + if (showVertically) { + mExpandedViewContainerMatrix.postTranslate( + 0f, expandingFromBubble.getTranslationY() + - expandingFromBubbleAt); + } else { + mExpandedViewContainerMatrix.postTranslate( + expandingFromBubble.getTranslationX() + - expandingFromBubbleAt, 0f); + } + } + + mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); + + // Hide early so we don't have a tiny little expanded view still visible at the + // end of the scale animation. + if (mExpandedViewContainerMatrix.getScaleX() < 0.05f) { + mExpandedViewContainer.setVisibility(View.INVISIBLE); + } + }) + .withEndActions(() -> { + final BubbleViewProvider previouslySelected = mExpandedBubble; + beforeExpandedViewAnimation(); + if (mManageEduView != null) { + mManageEduView.hide(false /* fromExpansion */); + } + + if (DEBUG_BUBBLE_STACK_VIEW) { + Log.d(TAG, "animateCollapse"); + Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(), + mExpandedBubble)); + } + updateOverflowVisibility(); + + afterExpandedViewAnimation(); + if (previouslySelected != null) { + previouslySelected.setContentVisibility(false); + } + + if (mPositioner.showingInTaskbar()) { + mTaskbarScrim.setVisibility(GONE); + } + }) + .start(); + } + + private void animateSwitchBubbles() { + // If we're no longer expanded, this is meaningless. + if (!mIsExpanded) { + return; + } + + mIsBubbleSwitchAnimating = true; + + // The surface contains a screenshot of the animating out bubble, so we just need to animate + // it out (and then release the GraphicBuffer). + PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel(); + PhysicsAnimator animator = PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer) + .spring(DynamicAnimation.SCALE_X, 0f, mScaleOutSpringConfig) + .spring(DynamicAnimation.SCALE_Y, 0f, mScaleOutSpringConfig) + .withEndActions(this::releaseAnimatingOutBubbleBuffer); + + if (mPositioner.showBubblesVertically()) { + float translationX = mStackAnimationController.isStackOnLeftSide() + ? mAnimatingOutSurfaceContainer.getTranslationX() + mBubbleSize * 2 + : mAnimatingOutSurfaceContainer.getTranslationX(); + animator.spring(DynamicAnimation.TRANSLATION_X, + translationX, + mTranslateSpringConfig) + .start(); + } else { + animator.spring(DynamicAnimation.TRANSLATION_Y, + mAnimatingOutSurfaceContainer.getTranslationY() - mBubbleSize * 2, + mTranslateSpringConfig) + .start(); + } + + boolean isOverflow = mExpandedBubble != null + && mExpandedBubble.getKey().equals(BubbleOverflow.KEY); + float expandingFromBubbleDestination = + mExpandedAnimationController.getBubbleXOrYForOrientation(isOverflow + ? getBubbleCount() + : mBubbleData.getBubbles().indexOf(mExpandedBubble)); + + mExpandedViewContainer.setAlpha(1f); + mExpandedViewContainer.setVisibility(View.VISIBLE); + + if (mPositioner.showBubblesVertically()) { + float pivotX; + float pivotY = expandingFromBubbleDestination + mBubbleSize / 2f; + if (mStackOnLeftOrWillBe) { + pivotX = mPositioner.getAvailableRect().left + mBubbleSize + mExpandedViewPadding; + } else { + pivotX = mPositioner.getAvailableRect().right - mBubbleSize - mExpandedViewPadding; + + } + mExpandedViewContainerMatrix.setScale( + 0f, 0f, + pivotX, pivotY); + } else { + mExpandedViewContainerMatrix.setScale( + 0f, 0f, + expandingFromBubbleDestination + mBubbleSize / 2f, + getExpandedViewY()); + } + + mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); + + mDelayedAnimationExecutor.executeDelayed(() -> { + if (!mIsExpanded) { + mIsBubbleSwitchAnimating = false; + return; + } + + PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); + PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) + .spring(AnimatableScaleMatrix.SCALE_X, + AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), + mScaleInSpringConfig) + .spring(AnimatableScaleMatrix.SCALE_Y, + AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), + mScaleInSpringConfig) + .addUpdateListener((target, values) -> { + mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); + }) + .withEndActions(() -> { + if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false); + } + + mIsBubbleSwitchAnimating = false; + }) + .start(); + }, 25); + } + + /** + * Cancels any delayed steps for expand/collapse and bubble switch animations, and resets the is + * animating flags for those animations. + */ + private void cancelDelayedExpandCollapseSwitchAnimations() { + mDelayedAnimationExecutor.removeAllCallbacks(); + + mIsExpansionAnimating = false; + mIsBubbleSwitchAnimating = false; + } + + private void cancelAllExpandCollapseSwitchAnimations() { + cancelDelayedExpandCollapseSwitchAnimations(); + + PhysicsAnimator.getInstance(mAnimatingOutSurfaceView).cancel(); + PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); + + mExpandedViewContainer.setAnimationMatrix(null); + } + + private void notifyExpansionChanged(BubbleViewProvider bubble, boolean expanded) { + if (mExpandListener != null && bubble != null) { + mExpandListener.onBubbleExpandChanged(expanded, bubble.getKey()); + } + } + + /** Moves the bubbles out of the way if they're going to be over the keyboard. */ + public void onImeVisibilityChanged(boolean visible, int height) { + mStackAnimationController.setImeHeight(visible ? height + mImeOffset : 0); + + if (!mIsExpanded && getBubbleCount() > 0) { + final float stackDestinationY = + mStackAnimationController.animateForImeVisibility(visible); + + // How far the stack is animating due to IME, we'll just animate the flyout by that + // much too. + final float stackDy = + stackDestinationY - mStackAnimationController.getStackPosition().y; + + // If the flyout is visible, translate it along with the bubble stack. + if (mFlyout.getVisibility() == VISIBLE) { + PhysicsAnimator.getInstance(mFlyout) + .spring(DynamicAnimation.TRANSLATION_Y, + mFlyout.getTranslationY() + stackDy, + FLYOUT_IME_ANIMATION_SPRING_CONFIG) + .start(); + } + } else if (mIsExpanded && mExpandedBubble != null + && mExpandedBubble.getExpandedView() != null) { + mExpandedBubble.getExpandedView().setImeVisible(visible); + } + } + + /** + * This method is called by {@link android.app.ActivityView} because the BubbleStackView has a + * higher Z-index than the ActivityView (so that dragged-out bubbles are visible over the AV). + * ActivityView is asking BubbleStackView to subtract the stack's bounds from the provided + * touchable region, so that the ActivityView doesn't consume events meant for the stack. Due to + * the special nature of ActivityView, it does not respect the standard + * {@link #dispatchTouchEvent} and {@link #onInterceptTouchEvent} methods typically used for + * this purpose. + * + * BubbleStackView is MATCH_PARENT, so that bubbles can be positioned via their translation + * properties for performance reasons. This means that the default implementation of this method + * subtracts the entirety of the screen from the ActivityView's touchable region, resulting in + * it not receiving any touch events. This was previously addressed by returning false in the + * stack's {@link View#canReceivePointerEvents()} method, but this precluded the use of any + * touch handlers in the stack or its child views. + * + * To support touch handlers, we're overriding this method to leave the ActivityView's touchable + * region alone. The only touchable part of the stack that can ever overlap the AV is a + * dragged-out bubble that is animating back into the row of bubbles. It's not worth continually + * updating the touchable region to allow users to grab a bubble while it completes its ~50ms + * animation back to the bubble row. + * + * NOTE: Any future additions to the stack that obscure the ActivityView region will need their + * bounds subtracted here in order to receive touch events. + */ + @Override + public void subtractObscuredTouchableRegion(Region touchableRegion, View view) { + // If the notification shade is expanded, or the manage menu is open, or we are showing + // manage bubbles user education, we shouldn't let the ActivityView steal any touch events + // from any location. + if (!mIsExpanded + || mShowingManage + || (mManageEduView != null + && mManageEduView.getVisibility() == VISIBLE)) { + touchableRegion.setEmpty(); + } + } + + /** + * If you're here because you're not receiving touch events on a view that is a descendant of + * BubbleStackView, and you think BSV is intercepting them - it's not! You need to subtract the + * bounds of the view in question in {@link #subtractObscuredTouchableRegion}. The ActivityView + * consumes all touch events within its bounds, even for views like the BubbleStackView that are + * above it. It ignores typical view touch handling methods like this one and + * dispatchTouchEvent. + */ + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + return super.onInterceptTouchEvent(ev); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (ev.getAction() != MotionEvent.ACTION_DOWN && ev.getActionIndex() != mPointerIndexDown) { + // Ignore touches from additional pointer indices. + return false; + } + + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mPointerIndexDown = ev.getActionIndex(); + } else if (ev.getAction() == MotionEvent.ACTION_UP + || ev.getAction() == MotionEvent.ACTION_CANCEL) { + mPointerIndexDown = -1; + } + + boolean dispatched = super.dispatchTouchEvent(ev); + + // If a new bubble arrives while the collapsed stack is being dragged, it will be positioned + // at the front of the stack (under the touch position). Subsequent ACTION_MOVE events will + // then be passed to the new bubble, which will not consume them since it hasn't received an + // ACTION_DOWN yet. Work around this by passing MotionEvents directly to the touch handler + // until the current gesture ends with an ACTION_UP event. + if (!dispatched && !mIsExpanded && mIsGestureInProgress) { + dispatched = mBubbleTouchListener.onTouch(this /* view */, ev); + } + + mIsGestureInProgress = + ev.getAction() != MotionEvent.ACTION_UP + && ev.getAction() != MotionEvent.ACTION_CANCEL; + + return dispatched; + } + + void setFlyoutStateForDragLength(float deltaX) { + // This shouldn't happen, but if it does, just wait until the flyout lays out. This method + // is continually called. + if (mFlyout.getWidth() <= 0) { + return; + } + + final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); + mFlyoutDragDeltaX = deltaX; + + final float collapsePercent = + onLeft ? -deltaX / mFlyout.getWidth() : deltaX / mFlyout.getWidth(); + mFlyout.setCollapsePercent(Math.min(1f, Math.max(0f, collapsePercent))); + + // Calculate how to translate the flyout if it has been dragged too far in either direction. + float overscrollTranslation = 0f; + if (collapsePercent < 0f || collapsePercent > 1f) { + // Whether we are more than 100% transitioned to the dot. + final boolean overscrollingPastDot = collapsePercent > 1f; + + // Whether we are overscrolling physically to the left - this can either be pulling the + // flyout away from the stack (if the stack is on the right) or pushing it to the left + // after it has already become the dot. + final boolean overscrollingLeft = + (onLeft && collapsePercent > 1f) || (!onLeft && collapsePercent < 0f); + overscrollTranslation = + (overscrollingPastDot ? collapsePercent - 1f : collapsePercent * -1) + * (overscrollingLeft ? -1 : 1) + * (mFlyout.getWidth() / (FLYOUT_OVERSCROLL_ATTENUATION_FACTOR + // Attenuate the smaller dot less than the larger flyout. + / (overscrollingPastDot ? 2 : 1))); + } + + mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation); + } + + /** Passes the MotionEvent to the magnetized object and returns true if it was consumed. */ + private boolean passEventToMagnetizedObject(MotionEvent event) { + return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event); + } + + /** + * Dismisses the magnetized object - either an individual bubble, if we're expanded, or the + * stack, if we're collapsed. + */ + private void dismissMagnetizedObject() { + if (mIsExpanded) { + final View draggedOutBubbleView = (View) mMagnetizedObject.getUnderlyingObject(); + dismissBubbleIfExists(mBubbleData.getBubbleWithView(draggedOutBubbleView)); + } else { + mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE); + } + } + + private void dismissBubbleIfExists(@Nullable BubbleViewProvider bubble) { + if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { + mBubbleData.dismissBubbleWithKey(bubble.getKey(), Bubbles.DISMISS_USER_GESTURE); + } + } + + /** Prepares and starts the desaturate/darken animation on the bubble stack. */ + private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) { + mDesaturateAndDarkenTargetView = targetView; + + if (mDesaturateAndDarkenTargetView == null) { + return; + } + + if (desaturateAndDarken) { + // Use the animated paint for the bubbles. + mDesaturateAndDarkenTargetView.setLayerType( + View.LAYER_TYPE_HARDWARE, mDesaturateAndDarkenPaint); + mDesaturateAndDarkenAnimator.removeAllListeners(); + mDesaturateAndDarkenAnimator.start(); + } else { + mDesaturateAndDarkenAnimator.removeAllListeners(); + mDesaturateAndDarkenAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + // Stop using the animated paint. + resetDesaturationAndDarken(); + } + }); + mDesaturateAndDarkenAnimator.reverse(); + } + } + + private void resetDesaturationAndDarken() { + + mDesaturateAndDarkenAnimator.removeAllListeners(); + mDesaturateAndDarkenAnimator.cancel(); + + if (mDesaturateAndDarkenTargetView != null) { + mDesaturateAndDarkenTargetView.setLayerType(View.LAYER_TYPE_NONE, null); + mDesaturateAndDarkenTargetView = null; + } + } + + /** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */ + private void animateFlyoutCollapsed(boolean collapsed, float velX) { + final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); + // If the flyout was tapped, we want a higher stiffness for the collapse animation so it's + // faster. + mFlyoutTransitionSpring.getSpring().setStiffness( + (mBubbleToExpandAfterFlyoutCollapse != null) + ? SpringForce.STIFFNESS_MEDIUM + : SpringForce.STIFFNESS_LOW); + mFlyoutTransitionSpring + .setStartValue(mFlyoutDragDeltaX) + .setStartVelocity(velX) + .animateToFinalPosition(collapsed + ? (onLeft ? -mFlyout.getWidth() : mFlyout.getWidth()) + : 0f); + } + + /** + * Calculates the y position of the expanded view when it is expanded. + */ + float getExpandedViewY() { + final int top = mPositioner.getAvailableRect().top; + if (mPositioner.showBubblesVertically()) { + return top + mExpandedViewPadding; + } else { + return top + mBubbleSize + mBubblePaddingTop; + } + } + + private boolean shouldShowFlyout(Bubble bubble) { + Bubble.FlyoutMessage flyoutMessage = bubble.getFlyoutMessage(); + final BadgedImageView bubbleView = bubble.getIconView(); + if (flyoutMessage == null + || flyoutMessage.message == null + || !bubble.showFlyout() + || (mStackEduView != null && mStackEduView.getVisibility() == VISIBLE) + || isExpanded() + || mIsExpansionAnimating + || mIsGestureInProgress + || mBubbleToExpandAfterFlyoutCollapse != null + || bubbleView == null) { + if (bubbleView != null && mFlyout.getVisibility() != VISIBLE) { + bubbleView.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); + } + // Skip the message if none exists, we're expanded or animating expansion, or we're + // about to expand a bubble from the previous tapped flyout, or if bubble view is null. + return false; + } + return true; + } + + /** + * Animates in the flyout for the given bubble, if available, and then hides it after some time. + */ + @VisibleForTesting + void animateInFlyoutForBubble(Bubble bubble) { + if (!shouldShowFlyout(bubble)) { + return; + } + + mFlyoutDragDeltaX = 0f; + clearFlyoutOnHide(); + mAfterFlyoutHidden = () -> { + // Null it out to ensure it runs once. + mAfterFlyoutHidden = null; + + if (mBubbleToExpandAfterFlyoutCollapse != null) { + // User tapped on the flyout and we should expand + mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse); + mBubbleData.setExpanded(true); + mBubbleToExpandAfterFlyoutCollapse = null; + } + + // Stop suppressing the dot now that the flyout has morphed into the dot. + if (bubble.getIconView() != null) { + bubble.getIconView().removeDotSuppressionFlag( + BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); + } + // Hide the stack after a delay, if needed. + updateTemporarilyInvisibleAnimation(false /* hideImmediately */); + }; + + // Suppress the dot when we are animating the flyout. + bubble.getIconView().addDotSuppressionFlag( + BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); + + // Start flyout expansion. Post in case layout isn't complete and getWidth returns 0. + post(() -> { + // An auto-expanding bubble could have been posted during the time it takes to + // layout. + if (isExpanded() || bubble.getIconView() == null) { + return; + } + final Runnable expandFlyoutAfterDelay = () -> { + mAnimateInFlyout = () -> { + mFlyout.setVisibility(VISIBLE); + updateTemporarilyInvisibleAnimation(false /* hideImmediately */); + mFlyoutDragDeltaX = + mStackAnimationController.isStackOnLeftSide() + ? -mFlyout.getWidth() + : mFlyout.getWidth(); + animateFlyoutCollapsed(false /* collapsed */, 0 /* velX */); + mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); + }; + mFlyout.postDelayed(mAnimateInFlyout, 200); + }; + + + if (mFlyout.getVisibility() == View.VISIBLE) { + mFlyout.animateUpdate(bubble.getFlyoutMessage(), getWidth(), + mStackAnimationController.getStackPosition().y); + } else { + mFlyout.setVisibility(INVISIBLE); + mFlyout.setupFlyoutStartingAsDot(bubble.getFlyoutMessage(), + mStackAnimationController.getStackPosition(), getWidth(), + mStackAnimationController.isStackOnLeftSide(), + bubble.getIconView().getDotColor() /* dotColor */, + expandFlyoutAfterDelay /* onLayoutComplete */, + mAfterFlyoutHidden, + bubble.getIconView().getDotCenter(), + !bubble.showDot(), + mPositioner); + } + mFlyout.bringToFront(); + }); + mFlyout.removeCallbacks(mHideFlyout); + mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); + logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT); + } + + /** Hide the flyout immediately and cancel any pending hide runnables. */ + private void hideFlyoutImmediate() { + clearFlyoutOnHide(); + mFlyout.removeCallbacks(mAnimateInFlyout); + mFlyout.removeCallbacks(mHideFlyout); + mFlyout.hideFlyout(); + } + + private void clearFlyoutOnHide() { + mFlyout.removeCallbacks(mAnimateInFlyout); + if (mAfterFlyoutHidden == null) { + return; + } + mAfterFlyoutHidden.run(); + mAfterFlyoutHidden = null; + } + + /** + * Fills the Rect with the touchable region of the bubbles. This will be used by WindowManager + * to decide which touch events go to Bubbles. + * + * Bubbles is below the status bar/notification shade but above application windows. If you're + * trying to get touch events from the status bar or another higher-level window layer, you'll + * need to re-order TYPE_BUBBLES in WindowManagerPolicy so that we have the opportunity to steal + * them. + */ + public void getTouchableRegion(Rect outRect) { + if (mStackEduView != null && mStackEduView.getVisibility() == VISIBLE) { + // When user education shows then capture all touches + outRect.set(0, 0, getWidth(), getHeight()); + return; + } + + if (!mIsExpanded) { + if (getBubbleCount() > 0 || mBubbleData.isShowingOverflow()) { + mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect); + // Increase the touch target size of the bubble + outRect.top -= mBubbleTouchPadding; + outRect.left -= mBubbleTouchPadding; + outRect.right += mBubbleTouchPadding; + outRect.bottom += mBubbleTouchPadding; + } + } else { + mBubbleContainer.getBoundsOnScreen(outRect); + } + + if (mFlyout.getVisibility() == View.VISIBLE) { + final Rect flyoutBounds = new Rect(); + mFlyout.getBoundsOnScreen(flyoutBounds); + outRect.union(flyoutBounds); + } + } + + private void requestUpdate() { + if (mViewUpdatedRequested || mIsExpansionAnimating) { + return; + } + mViewUpdatedRequested = true; + getViewTreeObserver().addOnPreDrawListener(mViewUpdater); + invalidate(); + } + + private void showManageMenu(boolean show) { + mShowingManage = show; + + // This should not happen, since the manage menu is only visible when there's an expanded + // bubble. If we end up in this state, just hide the menu immediately. + if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { + mManageMenu.setVisibility(View.INVISIBLE); + return; + } + + // If available, update the manage menu's settings option with the expanded bubble's app + // name and icon. + if (show && mBubbleData.hasBubbleInStackWithKey(mExpandedBubble.getKey())) { + final Bubble bubble = mBubbleData.getBubbleInStackWithKey(mExpandedBubble.getKey()); + mManageSettingsIcon.setImageDrawable(bubble.getAppBadge()); + mManageSettingsText.setText(getResources().getString( + R.string.bubbles_app_settings, bubble.getAppName())); + } + + mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect); + + final boolean isLtr = + getResources().getConfiguration().getLayoutDirection() == LAYOUT_DIRECTION_LTR; + + // When the menu is open, it should be at these coordinates. The menu pops out to the right + // in LTR and to the left in RTL. + final float targetX = isLtr ? mTempRect.left : mTempRect.right - mManageMenu.getWidth(); + final float targetY = mTempRect.bottom - mManageMenu.getHeight(); + + final float xOffsetForAnimation = (isLtr ? 1 : -1) * mManageMenu.getWidth() / 4f; + if (show) { + mManageMenu.setScaleX(0.5f); + mManageMenu.setScaleY(0.5f); + mManageMenu.setTranslationX(targetX - xOffsetForAnimation); + mManageMenu.setTranslationY(targetY + mManageMenu.getHeight() / 4f); + mManageMenu.setAlpha(0f); + + PhysicsAnimator.getInstance(mManageMenu) + .spring(DynamicAnimation.ALPHA, 1f) + .spring(DynamicAnimation.SCALE_X, 1f) + .spring(DynamicAnimation.SCALE_Y, 1f) + .spring(DynamicAnimation.TRANSLATION_X, targetX) + .spring(DynamicAnimation.TRANSLATION_Y, targetY) + .withEndActions(() -> { + View child = mManageMenu.getChildAt(0); + child.requestAccessibilityFocus(); + // Update the AV's obscured touchable region for the new visibility state. + mExpandedBubble.getExpandedView().updateObscuredTouchableRegion(); + }) + .start(); + + mManageMenu.setVisibility(View.VISIBLE); + } else { + PhysicsAnimator.getInstance(mManageMenu) + .spring(DynamicAnimation.ALPHA, 0f) + .spring(DynamicAnimation.SCALE_X, 0.5f) + .spring(DynamicAnimation.SCALE_Y, 0.5f) + .spring(DynamicAnimation.TRANSLATION_X, targetX - xOffsetForAnimation) + .spring(DynamicAnimation.TRANSLATION_Y, targetY + mManageMenu.getHeight() / 4f) + .withEndActions(() -> { + mManageMenu.setVisibility(View.INVISIBLE); + if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + // Update the AV's obscured touchable region for the new state. + mExpandedBubble.getExpandedView().updateObscuredTouchableRegion(); + } + }) + .start(); + } + } + + private void updateExpandedBubble() { + if (DEBUG_BUBBLE_STACK_VIEW) { + Log.d(TAG, "updateExpandedBubble()"); + } + + mExpandedViewContainer.removeAllViews(); + if (mIsExpanded && mExpandedBubble != null + && mExpandedBubble.getExpandedView() != null) { + BubbleExpandedView bev = mExpandedBubble.getExpandedView(); + bev.setContentVisibility(false); + mExpandedViewContainerMatrix.setScaleX(0f); + mExpandedViewContainerMatrix.setScaleY(0f); + mExpandedViewContainerMatrix.setTranslate(0f, 0f); + mExpandedViewContainer.setVisibility(View.INVISIBLE); + mExpandedViewContainer.setAlpha(0f); + mExpandedViewContainer.addView(bev); + bev.setManageClickListener((view) -> showManageMenu(!mShowingManage)); + + if (!mIsExpansionAnimating) { + mSurfaceSynchronizer.syncSurfaceAndRun(() -> { + post(this::animateSwitchBubbles); + }); + } + } + } + + /** + * Requests a snapshot from the currently expanded bubble's ActivityView and displays it in a + * SurfaceView. This allows us to load a newly expanded bubble's Activity into the ActivityView, + * while animating the (screenshot of the) previously selected bubble's content away. + * + * @param onComplete Callback to run once we're done here - called with 'false' if something + * went wrong, or 'true' if the SurfaceView is now showing a screenshot of the + * expanded bubble. + */ + private void screenshotAnimatingOutBubbleIntoSurface(Consumer<Boolean> onComplete) { + if (!mIsExpanded || mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { + // You can't animate null. + onComplete.accept(false); + return; + } + + final BubbleExpandedView animatingOutExpandedView = mExpandedBubble.getExpandedView(); + + // Release the previous screenshot if it hasn't been released already. + if (mAnimatingOutBubbleBuffer != null) { + releaseAnimatingOutBubbleBuffer(); + } + + try { + mAnimatingOutBubbleBuffer = animatingOutExpandedView.snapshotActivitySurface(); + } catch (Exception e) { + // If we fail for any reason, print the stack trace and then notify the callback of our + // failure. This is not expected to occur, but it's not worth crashing over. + Log.wtf(TAG, e); + onComplete.accept(false); + } + + if (mAnimatingOutBubbleBuffer == null + || mAnimatingOutBubbleBuffer.getHardwareBuffer() == null) { + // While no exception was thrown, we were unable to get a snapshot. + onComplete.accept(false); + return; + } + + // Make sure the surface container's properties have been reset. + PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel(); + mAnimatingOutSurfaceContainer.setScaleX(1f); + mAnimatingOutSurfaceContainer.setScaleY(1f); + mAnimatingOutSurfaceContainer.setTranslationX(mExpandedViewContainer.getPaddingLeft()); + mAnimatingOutSurfaceContainer.setTranslationY(0); + + final int[] activityViewLocation = + mExpandedBubble.getExpandedView().getTaskViewLocationOnScreen(); + final int[] surfaceViewLocation = mAnimatingOutSurfaceView.getLocationOnScreen(); + + // Translate the surface to overlap the real ActivityView. + mAnimatingOutSurfaceContainer.setTranslationY( + activityViewLocation[1] - surfaceViewLocation[1]); + + // Set the width/height of the SurfaceView to match the snapshot. + mAnimatingOutSurfaceView.getLayoutParams().width = + mAnimatingOutBubbleBuffer.getHardwareBuffer().getWidth(); + mAnimatingOutSurfaceView.getLayoutParams().height = + mAnimatingOutBubbleBuffer.getHardwareBuffer().getHeight(); + mAnimatingOutSurfaceView.requestLayout(); + + // Post to wait for layout. + post(() -> { + // The buffer might have been destroyed if the user is mashing on bubbles, that's okay. + if (mAnimatingOutBubbleBuffer == null + || mAnimatingOutBubbleBuffer.getHardwareBuffer() == null + || mAnimatingOutBubbleBuffer.getHardwareBuffer().isClosed()) { + onComplete.accept(false); + return; + } + + if (!mIsExpanded) { + onComplete.accept(false); + return; + } + + // Attach the buffer! We're now displaying the snapshot. + mAnimatingOutSurfaceView.getHolder().getSurface().attachAndQueueBufferWithColorSpace( + mAnimatingOutBubbleBuffer.getHardwareBuffer(), + mAnimatingOutBubbleBuffer.getColorSpace()); + + mSurfaceSynchronizer.syncSurfaceAndRun(() -> post(() -> onComplete.accept(true))); + }); + } + + /** + * Releases the buffer containing the screenshot of the animating-out bubble, if it exists and + * isn't yet destroyed. + */ + private void releaseAnimatingOutBubbleBuffer() { + if (mAnimatingOutBubbleBuffer != null + && !mAnimatingOutBubbleBuffer.getHardwareBuffer().isClosed()) { + mAnimatingOutBubbleBuffer.getHardwareBuffer().close(); + } + } + + private void updateExpandedView() { + if (DEBUG_BUBBLE_STACK_VIEW) { + Log.d(TAG, "updateExpandedView: mIsExpanded=" + mIsExpanded); + } + + // Need to update the padding around the view for any insets + Insets insets = mPositioner.getInsets(); + int leftPadding = insets.left + mExpandedViewPadding; + int rightPadding = insets.right + mExpandedViewPadding; + if (mPositioner.showBubblesVertically()) { + if (!mStackAnimationController.isStackOnLeftSide()) { + rightPadding += mPointerHeight + mBubbleSize; + } else { + leftPadding += mPointerHeight + mBubbleSize; + } + } + mExpandedViewContainer.setPadding(leftPadding, 0, rightPadding, 0); + mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE); + if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + mExpandedViewContainer.setTranslationY(getExpandedViewY()); + mExpandedViewContainer.setTranslationX(0f); + mExpandedBubble.getExpandedView().updateView( + mExpandedViewContainer.getLocationOnScreen()); + } + + mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); + updateBadgesAndZOrder(false /* setBadgeForCollapsedStack */); + } + + /** + * Sets the appropriate Z-order, badge, and dot position for each bubble in the stack. + * Animate dot and badge changes. + */ + private void updateBadgesAndZOrder(boolean setBadgeForCollapsedStack) { + int bubbleCount = getBubbleCount(); + for (int i = 0; i < bubbleCount; i++) { + BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i); + bv.setZ((mMaxBubbles * mBubbleElevation) - i); + if (mIsExpanded) { + // If we're not displaying vertically, we always show the badge on the left. + boolean onLeft = mPositioner.showBubblesVertically() && !mStackOnLeftOrWillBe; + bv.showDotAndBadge(onLeft); + } else if (setBadgeForCollapsedStack) { + if (i == 0) { + bv.showDotAndBadge(!mStackOnLeftOrWillBe); + } else { + bv.hideDotAndBadge(!mStackOnLeftOrWillBe); + } + } + } + } + + private void updatePointerPosition() { + if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { + return; + } + int index = getBubbleIndex(mExpandedBubble); + if (index == -1) { + return; + } + float bubblePosition = mExpandedAnimationController.getBubbleXOrYForOrientation(index); + if (mPositioner.showBubblesVertically()) { + float x = mStackOnLeftOrWillBe + ? mPositioner.getAvailableRect().left + : mPositioner.getAvailableRect().right + - mExpandedViewContainer.getPaddingRight() + - mPointerHeight; + float bubbleCenter = bubblePosition - getExpandedViewY() + (mBubbleSize / 2f); + mExpandedBubble.getExpandedView().setPointerPosition( + x, + bubbleCenter, + true, + mStackOnLeftOrWillBe); + } else { + float bubbleCenter = bubblePosition + (mBubbleSize / 2f); + mExpandedBubble.getExpandedView().setPointerPosition( + bubbleCenter, + getExpandedViewY(), + false, + mStackOnLeftOrWillBe); + } + } + + /** + * @return the number of bubbles in the stack view. + */ + public int getBubbleCount() { + // Subtract 1 for the overflow button that is always in the bubble container. + return mBubbleContainer.getChildCount() - 1; + } + + /** + * Finds the bubble index within the stack. + * + * @param provider the bubble view provider with the bubble to look up. + * @return the index of the bubble view within the bubble stack. The range of the position + * is between 0 and the bubble count minus 1. + */ + int getBubbleIndex(@Nullable BubbleViewProvider provider) { + if (provider == null) { + return 0; + } + return mBubbleContainer.indexOfChild(provider.getIconView()); + } + + /** + * @return the normalized x-axis position of the bubble stack rounded to 4 decimal places. + */ + public float getNormalizedXPosition() { + return new BigDecimal(getStackPosition().x / mPositioner.getAvailableRect().width()) + .setScale(4, RoundingMode.CEILING.HALF_UP) + .floatValue(); + } + + /** + * @return the normalized y-axis position of the bubble stack rounded to 4 decimal places. + */ + public float getNormalizedYPosition() { + return new BigDecimal(getStackPosition().y / mPositioner.getAvailableRect().height()) + .setScale(4, RoundingMode.CEILING.HALF_UP) + .floatValue(); + } + + /** @return the position of the bubble stack. */ + public PointF getStackPosition() { + return mStackAnimationController.getStackPosition(); + } + + /** + * Logs the bubble UI event. + * + * @param provider the bubble view provider that is being interacted on. Null value indicates + * that the user interaction is not specific to one bubble. + * @param action the user interaction enum. + */ + private void logBubbleEvent(@Nullable BubbleViewProvider provider, int action) { + mBubbleData.logBubbleEvent(provider, + action, + mContext.getApplicationInfo().packageName, + getBubbleCount(), + getBubbleIndex(provider), + getNormalizedXPosition(), + getNormalizedYPosition()); + } + + /** For debugging only */ + List<Bubble> getBubblesOnScreen() { + List<Bubble> bubbles = new ArrayList<>(); + for (int i = 0; i < getBubbleCount(); i++) { + View child = mBubbleContainer.getChildAt(i); + if (child instanceof BadgedImageView) { + String key = ((BadgedImageView) child).getKey(); + Bubble bubble = mBubbleData.getBubbleInStackWithKey(key); + bubbles.add(bubble); + } + } + return bubbles; + } + + /** + * Representation of stack position that uses relative properties rather than absolute + * coordinates. This is used to maintain similar stack positions across configuration changes. + */ + public static class RelativeStackPosition { + /** Whether to place the stack at the leftmost allowed position. */ + private boolean mOnLeft; + + /** + * How far down the vertically allowed region to place the stack. For example, if the stack + * allowed region is between y = 100 and y = 1100 and this is 0.2f, we'll place the stack at + * 100 + (0.2f * 1000) = 300. + */ + private float mVerticalOffsetPercent; + + public RelativeStackPosition(boolean onLeft, float verticalOffsetPercent) { + mOnLeft = onLeft; + mVerticalOffsetPercent = clampVerticalOffsetPercent(verticalOffsetPercent); + } + + /** Constructs a relative position given a region and a point in that region. */ + public RelativeStackPosition(PointF position, RectF region) { + mOnLeft = position.x < region.width() / 2; + mVerticalOffsetPercent = + clampVerticalOffsetPercent((position.y - region.top) / region.height()); + } + + /** Ensures that the offset percent is between 0f and 1f. */ + private float clampVerticalOffsetPercent(float offsetPercent) { + return Math.max(0f, Math.min(1f, offsetPercent)); + } + + /** + * Given an allowable stack position region, returns the point within that region + * represented by this relative position. + */ + public PointF getAbsolutePositionInRegion(RectF region) { + return new PointF( + mOnLeft ? region.left : region.right, + region.top + mVerticalOffsetPercent * region.height()); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java new file mode 100644 index 000000000000..c5a712e271e4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java @@ -0,0 +1,226 @@ +/* + * 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.bubbles; + +import static com.android.wm.shell.bubbles.BadgedImageView.DEFAULT_PATH_SIZE; +import static com.android.wm.shell.bubbles.BadgedImageView.WHITE_SCRIM_ALPHA; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; + +import android.annotation.NonNull; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ShortcutInfo; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Path; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.os.AsyncTask; +import android.util.Log; +import android.util.PathParser; +import android.view.LayoutInflater; + +import androidx.annotation.Nullable; + +import com.android.internal.graphics.ColorUtils; +import com.android.launcher3.icons.BitmapInfo; +import com.android.wm.shell.R; + +import java.lang.ref.WeakReference; +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * Simple task to inflate views & load necessary info to display a bubble. + */ +public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask.BubbleViewInfo> { + private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleViewInfoTask" : TAG_BUBBLES; + + + /** + * Callback to find out when the bubble has been inflated & necessary data loaded. + */ + public interface Callback { + /** + * Called when data has been loaded for the bubble. + */ + void onBubbleViewsReady(Bubble bubble); + } + + private Bubble mBubble; + private WeakReference<Context> mContext; + private WeakReference<BubbleController> mController; + private WeakReference<BubbleStackView> mStackView; + private BubbleIconFactory mIconFactory; + private boolean mSkipInflation; + private Callback mCallback; + private Executor mMainExecutor; + + /** + * Creates a task to load information for the provided {@link Bubble}. Once all info + * is loaded, {@link Callback} is notified. + */ + BubbleViewInfoTask(Bubble b, + Context context, + BubbleController controller, + BubbleStackView stackView, + BubbleIconFactory factory, + boolean skipInflation, + Callback c, + Executor mainExecutor) { + mBubble = b; + mContext = new WeakReference<>(context); + mController = new WeakReference<>(controller); + mStackView = new WeakReference<>(stackView); + mIconFactory = factory; + mSkipInflation = skipInflation; + mCallback = c; + mMainExecutor = mainExecutor; + } + + @Override + protected BubbleViewInfo doInBackground(Void... voids) { + return BubbleViewInfo.populate(mContext.get(), mController.get(), mStackView.get(), + mIconFactory, mBubble, mSkipInflation); + } + + @Override + protected void onPostExecute(BubbleViewInfo viewInfo) { + if (isCancelled() || viewInfo == null) { + return; + } + mMainExecutor.execute(() -> { + mBubble.setViewInfo(viewInfo); + if (mCallback != null) { + mCallback.onBubbleViewsReady(mBubble); + } + }); + } + + /** + * Info necessary to render a bubble. + */ + static class BubbleViewInfo { + BadgedImageView imageView; + BubbleExpandedView expandedView; + ShortcutInfo shortcutInfo; + String appName; + Bitmap bubbleBitmap; + Drawable badgeDrawable; + int dotColor; + Path dotPath; + Bubble.FlyoutMessage flyoutMessage; + + @Nullable + static BubbleViewInfo populate(Context c, BubbleController controller, + BubbleStackView stackView, BubbleIconFactory iconFactory, Bubble b, + boolean skipInflation) { + BubbleViewInfo info = new BubbleViewInfo(); + + // View inflation: only should do this once per bubble + if (!skipInflation && !b.isInflated()) { + LayoutInflater inflater = LayoutInflater.from(c); + info.imageView = (BadgedImageView) inflater.inflate( + R.layout.bubble_view, stackView, false /* attachToRoot */); + info.imageView.initialize(controller.getPositioner()); + + info.expandedView = (BubbleExpandedView) inflater.inflate( + R.layout.bubble_expanded_view, stackView, false /* attachToRoot */); + info.expandedView.initialize(controller, stackView, false /* isOverflow */); + } + + if (b.getShortcutInfo() != null) { + info.shortcutInfo = b.getShortcutInfo(); + } + + // App name & app icon + PackageManager pm = c.getPackageManager(); + ApplicationInfo appInfo; + Drawable badgedIcon; + Drawable appIcon; + try { + appInfo = pm.getApplicationInfo( + b.getPackageName(), + PackageManager.MATCH_UNINSTALLED_PACKAGES + | PackageManager.MATCH_DISABLED_COMPONENTS + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE + | PackageManager.MATCH_DIRECT_BOOT_AWARE); + if (appInfo != null) { + info.appName = String.valueOf(pm.getApplicationLabel(appInfo)); + } + appIcon = pm.getApplicationIcon(b.getPackageName()); + badgedIcon = pm.getUserBadgedIcon(appIcon, b.getUser()); + } catch (PackageManager.NameNotFoundException exception) { + // If we can't find package... don't think we should show the bubble. + Log.w(TAG, "Unable to find package: " + b.getPackageName()); + return null; + } + + // Badged bubble image + Drawable bubbleDrawable = iconFactory.getBubbleDrawable(c, info.shortcutInfo, + b.getIcon()); + if (bubbleDrawable == null) { + // Default to app icon + bubbleDrawable = appIcon; + } + + BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon, + b.isImportantConversation()); + info.badgeDrawable = badgedIcon; + info.bubbleBitmap = iconFactory.createBadgedIconBitmap(bubbleDrawable, + null /* user */, + true /* shrinkNonAdaptiveIcons */).icon; + + // Dot color & placement + Path iconPath = PathParser.createPathFromPathData( + c.getResources().getString(com.android.internal.R.string.config_icon_mask)); + Matrix matrix = new Matrix(); + float scale = iconFactory.getNormalizer().getScale(bubbleDrawable, + null /* outBounds */, null /* path */, null /* outMaskShape */); + float radius = DEFAULT_PATH_SIZE / 2f; + matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */, + radius /* pivot y */); + iconPath.transform(matrix); + info.dotPath = iconPath; + info.dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, + Color.WHITE, WHITE_SCRIM_ALPHA); + + // Flyout + info.flyoutMessage = b.getFlyoutMessage(); + if (info.flyoutMessage != null) { + info.flyoutMessage.senderAvatar = + loadSenderAvatar(c, info.flyoutMessage.senderIcon); + } + return info; + } + } + + @Nullable + static Drawable loadSenderAvatar(@NonNull final Context context, @Nullable final Icon icon) { + Objects.requireNonNull(context); + if (icon == null) return null; + if (icon.getType() == Icon.TYPE_URI || icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) { + context.grantUriPermission(context.getPackageName(), + icon.getUri(), Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + return icon.loadDrawable(context); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java new file mode 100644 index 000000000000..ec900be13658 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java @@ -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. + */ + +package com.android.wm.shell.bubbles; + +import android.graphics.Bitmap; +import android.graphics.Path; +import android.graphics.drawable.Drawable; +import android.view.View; + +import androidx.annotation.Nullable; + +/** + * Interface to represent actual Bubbles and UI elements that act like bubbles, like BubbleOverflow. + */ +public interface BubbleViewProvider { + @Nullable BubbleExpandedView getExpandedView(); + + void setContentVisibility(boolean visible); + + @Nullable View getIconView(); + + String getKey(); + + /** Bubble icon bitmap with no badge and no dot. */ + Bitmap getBubbleIcon(); + + /** App badge drawable to draw above bubble icon. */ + @Nullable Drawable getAppBadge(); + + /** Path of normalized bubble icon to draw dot on. */ + Path getDotPath(); + + int getDotColor(); + + boolean showDot(); + + int getTaskId(); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java new file mode 100644 index 000000000000..6a1026bb24fe --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -0,0 +1,275 @@ +/* + * 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.bubbles; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.content.res.Configuration; +import android.os.Bundle; +import android.os.Looper; +import android.service.notification.NotificationListenerService.RankingMap; +import android.util.ArraySet; +import android.view.View; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; + +import com.android.wm.shell.common.annotations.ExternalThread; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +/** + * Interface to engage bubbles feature. + */ +@ExternalThread +public interface Bubbles { + + @Retention(SOURCE) + @IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED, + DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE, + DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT, + DISMISS_OVERFLOW_MAX_REACHED, DISMISS_SHORTCUT_REMOVED, DISMISS_PACKAGE_REMOVED, + DISMISS_NO_BUBBLE_UP, DISMISS_RELOAD_FROM_DISK}) + @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) + @interface DismissReason {} + + int DISMISS_USER_GESTURE = 1; + int DISMISS_AGED = 2; + int DISMISS_TASK_FINISHED = 3; + int DISMISS_BLOCKED = 4; + int DISMISS_NOTIF_CANCEL = 5; + int DISMISS_ACCESSIBILITY_ACTION = 6; + int DISMISS_NO_LONGER_BUBBLE = 7; + int DISMISS_USER_CHANGED = 8; + int DISMISS_GROUP_CANCELLED = 9; + int DISMISS_INVALID_INTENT = 10; + int DISMISS_OVERFLOW_MAX_REACHED = 11; + int DISMISS_SHORTCUT_REMOVED = 12; + int DISMISS_PACKAGE_REMOVED = 13; + int DISMISS_NO_BUBBLE_UP = 14; + int DISMISS_RELOAD_FROM_DISK = 15; + + /** + * @return {@code true} if there is a bubble associated with the provided key and if its + * notification is hidden from the shade or there is a group summary associated with the + * provided key that is hidden from the shade because it has been dismissed but still has child + * bubbles active. + */ + boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey); + + /** + * @return {@code true} if the current notification entry same as selected bubble + * notification entry and the stack is currently expanded. + */ + boolean isBubbleExpanded(String key); + + /** @return {@code true} if stack of bubbles is expanded or not. */ + boolean isStackExpanded(); + + /** + * Removes a group key indicating that the summary for this group should no longer be + * suppressed. + * + * @param callback If removed, this callback will be called with the summary key of the group + */ + void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback, + Executor callbackExecutor); + + /** Tell the stack of bubbles to collapse. */ + void collapseStack(); + + /** Tell the controller need update its UI to fit theme. */ + void updateForThemeChanges(); + + /** + * Request the stack expand if needed, then select the specified Bubble as current. + * If no bubble exists for this entry, one is created. + * + * @param entry the notification for the bubble to be selected + */ + void expandStackAndSelectBubble(BubbleEntry entry); + + /** Called for any taskbar changes. */ + void onTaskbarChanged(Bundle b); + + /** Open the overflow view. */ + void openBubbleOverflow(); + + /** + * We intercept notification entries (including group summaries) dismissed by the user when + * there is an active bubble associated with it. We do this so that developers can still + * cancel it (and hence the bubbles associated with it). However, these intercepted + * notifications should then be hidden from the shade since the user has cancelled them, so we + * {@link Bubble#setSuppressNotification}. For the case of suppressed summaries, we also add + * {@link BubbleData#addSummaryToSuppress}. + * + * @param entry the notification of the BubbleEntry should be removed. + * @param children the list of child notification of the BubbleEntry from 1st param entry, + * this will be null if entry does have no children. + * @param removeCallback the remove callback for SystemUI side to remove notification, the int + * number should be list position of children list and use -1 for + * removing the parent notification. + * + * @return true if we want to intercept the dismissal of the entry, else false. + */ + boolean handleDismissalInterception(BubbleEntry entry, @Nullable List<BubbleEntry> children, + IntConsumer removeCallback); + + /** Set the proxy to commnuicate with SysUi side components. */ + void setSysuiProxy(SysuiProxy proxy); + + /** + * Set the scrim view for bubbles. + * + * @param callback The callback made with the executor and the executor's looper that the view + * will be running on. + **/ + void setBubbleScrim(View view, BiConsumer<Executor, Looper> callback); + + /** Set a listener to be notified of bubble expand events. */ + void setExpandListener(BubbleExpandListener listener); + + /** + * Called when new notification entry added. + * + * @param entry the {@link BubbleEntry} by the notification. + */ + void onEntryAdded(BubbleEntry entry); + + /** + * Called when new notification entry updated. + * + * @param entry the {@link BubbleEntry} by the notification. + * @param shouldBubbleUp {@code true} if this notification should bubble up. + */ + void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp); + + /** + * Called when new notification entry removed. + * + * @param entry the {@link BubbleEntry} by the notification. + */ + void onEntryRemoved(BubbleEntry entry); + + /** + * Called when NotificationListener has received adjusted notification rank and reapplied + * filtering and sorting. This is used to dismiss or create bubbles based on changes in + * permissions on the notification channel or the global setting. + * + * @param rankingMap the updated ranking map from NotificationListenerService + */ + void onRankingUpdated(RankingMap rankingMap); + + /** + * Called when the status bar has become visible or invisible (either permanently or + * temporarily). + */ + void onStatusBarVisibilityChanged(boolean visible); + + /** Called when system zen mode state changed. */ + void onZenStateChanged(); + + /** + * Called when statusBar state changed. + * + * @param isShade {@code true} is state is SHADE. + */ + void onStatusBarStateChanged(boolean isShade); + + /** + * Called when the current user changed. + * + * @param newUserId the new user's id. + */ + void onUserChanged(int newUserId); + + /** + * Called when config changed. + * + * @param newConfig the new config. + */ + void onConfigChanged(Configuration newConfig); + + /** Description of current bubble state. */ + void dump(FileDescriptor fd, PrintWriter pw, String[] args); + + /** Listener to find out about stack expansion / collapse events. */ + interface BubbleExpandListener { + /** + * Called when the expansion state of the bubble stack changes. + * + * @param isExpanding whether it's expanding or collapsing + * @param key the notification key associated with bubble being expanded + */ + void onBubbleExpandChanged(boolean isExpanding, String key); + } + + /** Listener to be notified when a bubbles' notification suppression state changes.*/ + interface NotificationSuppressionChangedListener { + /** Called when the notification suppression state of a bubble changes. */ + void onBubbleNotificationSuppressionChange(Bubble bubble); + } + + /** Listener to be notified when a pending intent has been canceled for a bubble. */ + interface PendingIntentCanceledListener { + /** Called when the pending intent for a bubble has been canceled. */ + void onPendingIntentCanceled(Bubble bubble); + } + + /** Callback to tell SysUi components execute some methods. */ + interface SysuiProxy { + @Nullable + BubbleEntry getPendingOrActiveEntry(String key); + + List<BubbleEntry> getShouldRestoredEntries(ArraySet<String> savedBubbleKeys); + + boolean isNotificationShadeExpand(); + + boolean shouldBubbleUp(String key); + + void setNotificationInterruption(String key); + + void requestNotificationShadeTopUi(boolean requestTopUi, String componentTag); + + void notifyRemoveNotification(String key, int reason); + + void notifyInvalidateNotifications(String reason); + + void notifyMaybeCancelSummary(String key); + + void removeNotificationEntry(String key); + + void updateNotificationBubbleButton(String key); + + void updateNotificationSuppression(String key); + + void onStackExpandChanged(boolean shouldExpand); + + void onUnbubbleConversation(String key); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt new file mode 100644 index 000000000000..04b5ad6dddf9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt @@ -0,0 +1,101 @@ +/* + * 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.bubbles + +import android.content.Context +import android.graphics.drawable.TransitionDrawable +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY +import androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW +import com.android.wm.shell.R +import com.android.wm.shell.animation.PhysicsAnimator +import com.android.wm.shell.common.DismissCircleView + +/* + * View that handles interactions between DismissCircleView and BubbleStackView. + */ +class DismissView(context: Context) : FrameLayout(context) { + + var circle = DismissCircleView(context).apply { + val targetSize: Int = context.resources.getDimensionPixelSize(R.dimen.dismiss_circle_size) + val newParams = LayoutParams(targetSize, targetSize) + newParams.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL + setLayoutParams(newParams) + setTranslationY( + resources.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height).toFloat()) + } + + var isShowing = false + private val animator = PhysicsAnimator.getInstance(circle) + private val spring = PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY) + private val DISMISS_SCRIM_FADE_MS = 200 + init { + setLayoutParams(LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + resources.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height), + Gravity.BOTTOM)) + setPadding(0, 0, 0, resources.getDimensionPixelSize(R.dimen.floating_dismiss_bottom_margin)) + setClipToPadding(false) + setClipChildren(false) + setVisibility(View.INVISIBLE) + setBackgroundResource( + R.drawable.floating_dismiss_gradient_transition) + addView(circle) + } + + /** + * Animates this view in. + */ + fun show() { + if (isShowing) return + isShowing = true + bringToFront() + setZ(Short.MAX_VALUE - 1f) + setVisibility(View.VISIBLE) + (getBackground() as TransitionDrawable).startTransition(DISMISS_SCRIM_FADE_MS) + animator.cancel() + animator + .spring(DynamicAnimation.TRANSLATION_Y, 0f, spring) + .start() + } + + /** + * Animates this view out, as well as the circle that encircles the bubbles, if they + * were dragged into the target and encircled. + */ + fun hide() { + if (!isShowing) return + isShowing = false + (getBackground() as TransitionDrawable).reverseTransition(DISMISS_SCRIM_FADE_MS) + animator + .spring(DynamicAnimation.TRANSLATION_Y, height.toFloat(), + spring) + .withEndActions({ setVisibility(View.INVISIBLE) }) + .start() + } + + fun updateResources() { + val targetSize: Int = context.resources.getDimensionPixelSize(R.dimen.dismiss_circle_size) + circle.layoutParams.width = targetSize + circle.layoutParams.height = targetSize + circle.requestLayout() + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt new file mode 100644 index 000000000000..4cc67025fff4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt @@ -0,0 +1,146 @@ +/* + * 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.bubbles + +import android.content.Context +import android.graphics.Color +import android.graphics.Rect +import android.view.LayoutInflater +import android.view.View +import android.widget.Button +import android.widget.LinearLayout +import android.widget.TextView +import com.android.internal.util.ContrastColorUtil +import com.android.wm.shell.R +import com.android.wm.shell.animation.Interpolators + +/** + * User education view to highlight the manage button that allows a user to configure the settings + * for the bubble. Shown only the first time a user expands a bubble. + */ +class ManageEducationView constructor(context: Context) : LinearLayout(context) { + + private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleManageEducationView" + else BubbleDebugConfig.TAG_BUBBLES + + private val ANIMATE_DURATION: Long = 200 + private val ANIMATE_DURATION_SHORT: Long = 40 + + private val manageView by lazy { findViewById<View>(R.id.manage_education_view) } + private val manageButton by lazy { findViewById<Button>(R.id.manage) } + private val gotItButton by lazy { findViewById<Button>(R.id.got_it) } + private val titleTextView by lazy { findViewById<TextView>(R.id.user_education_title) } + private val descTextView by lazy { findViewById<TextView>(R.id.user_education_description) } + + private var isHiding = false + + init { + LayoutInflater.from(context).inflate(R.layout.bubbles_manage_button_education, this) + visibility = View.GONE + elevation = resources.getDimensionPixelSize(R.dimen.bubble_elevation).toFloat() + + // BubbleStackView forces LTR by default + // since most of Bubble UI direction depends on positioning by the user. + // This view actually lays out differently in RTL, so we set layout LOCALE here. + layoutDirection = View.LAYOUT_DIRECTION_LOCALE + } + + override fun setLayoutDirection(layoutDirection: Int) { + super.setLayoutDirection(layoutDirection) + setDrawableDirection() + } + + override fun onFinishInflate() { + super.onFinishInflate() + layoutDirection = resources.configuration.layoutDirection + setTextColor() + } + + private fun setTextColor() { + val typedArray = mContext.obtainStyledAttributes(intArrayOf(android.R.attr.colorAccent, + android.R.attr.textColorPrimaryInverse)) + val bgColor = typedArray.getColor(0 /* index */, Color.BLACK) + var textColor = typedArray.getColor(1 /* index */, Color.WHITE) + typedArray.recycle() + textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true) + titleTextView.setTextColor(textColor) + descTextView.setTextColor(textColor) + } + + private fun setDrawableDirection() { + manageView.setBackgroundResource( + if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL) + R.drawable.bubble_stack_user_education_bg_rtl + else R.drawable.bubble_stack_user_education_bg) + } + + /** + * If necessary, toggles the user education view for the manage button. This is shown when the + * bubble stack is expanded for the first time. + * + * @param show whether the user education view should show or not. + */ + fun show(expandedView: BubbleExpandedView, rect: Rect) { + if (visibility == VISIBLE) return + + alpha = 0f + visibility = View.VISIBLE + post { + expandedView.getManageButtonBoundsOnScreen(rect) + + manageButton + .setOnClickListener { + expandedView.findViewById<View>(R.id.settings_button).performClick() + hide(true /* isStackExpanding */) + } + gotItButton.setOnClickListener { hide(true /* isStackExpanding */) } + setOnClickListener { hide(true /* isStackExpanding */) } + + with(manageView) { + translationX = 0f + val inset = resources.getDimensionPixelSize( + R.dimen.bubbles_manage_education_top_inset) + translationY = (rect.top - manageView.height + inset).toFloat() + } + bringToFront() + animate() + .setDuration(ANIMATE_DURATION) + .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) + .alpha(1f) + } + setShouldShow(false) + } + + fun hide(isStackExpanding: Boolean) { + if (visibility != VISIBLE || isHiding) return + + animate() + .withStartAction { isHiding = true } + .alpha(0f) + .setDuration(if (isStackExpanding) ANIMATE_DURATION_SHORT else ANIMATE_DURATION) + .withEndAction { + isHiding = false + visibility = GONE + } + } + + private fun setShouldShow(shouldShow: Boolean) { + context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE) + .edit().putBoolean(PREF_MANAGED_EDUCATION, !shouldShow).apply() + } +} + +const val PREF_MANAGED_EDUCATION: String = "HasSeenBubblesManageOnboarding"
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ObjectWrapper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ObjectWrapper.java new file mode 100644 index 000000000000..528907f5e483 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ObjectWrapper.java @@ -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. + */ +package com.android.wm.shell.bubbles; + +import android.os.Binder; +import android.os.IBinder; + +// Copied from Launcher3 +/** + * Utility class to pass non-parcealable objects within same process using parcealable payload. + * + * It wraps the object in a binder as binders are singleton within a process + */ +public class ObjectWrapper<T> extends Binder { + + private T mObject; + + public ObjectWrapper(T object) { + mObject = object; + } + + public T get() { + return mObject; + } + + public void clear() { + mObject = null; + } + + public static IBinder wrap(Object obj) { + return new ObjectWrapper<>(obj); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt new file mode 100644 index 000000000000..cf0cefec401a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt @@ -0,0 +1,164 @@ +/* + * 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.bubbles + +import android.graphics.PointF +import android.os.Handler +import android.os.Looper +import android.view.MotionEvent +import android.view.VelocityTracker +import android.view.View +import android.view.ViewConfiguration +import kotlin.math.hypot + +/** + * Listener which receives [onDown], [onMove], and [onUp] events, with relevant information about + * the coordinates of the touch and the view relative to the initial ACTION_DOWN event and the + * view's initial position. + */ +abstract class RelativeTouchListener : View.OnTouchListener { + + /** + * Called when an ACTION_DOWN event is received for the given view. + * + * @return False if the object is not interested in MotionEvents at this time, or true if we + * should consume this event and subsequent events, and begin calling [onMove]. + */ + abstract fun onDown(v: View, ev: MotionEvent): Boolean + + /** + * Called when an ACTION_MOVE event is received for the given view. This signals that the view + * is being dragged. + * + * @param viewInitialX The view's translationX value when this touch gesture started. + * @param viewInitialY The view's translationY value when this touch gesture started. + * @param dx Horizontal distance covered since the initial ACTION_DOWN event, in pixels. + * @param dy Vertical distance covered since the initial ACTION_DOWN event, in pixels. + */ + abstract fun onMove( + v: View, + ev: MotionEvent, + viewInitialX: Float, + viewInitialY: Float, + dx: Float, + dy: Float + ) + + /** + * Called when an ACTION_UP event is received for the given view. This signals that a drag or + * fling gesture has completed. + * + * @param viewInitialX The view's translationX value when this touch gesture started. + * @param viewInitialY The view's translationY value when this touch gesture started. + * @param dx Horizontal distance covered, in pixels. + * @param dy Vertical distance covered, in pixels. + * @param velX The final horizontal velocity of the gesture, in pixels/second. + * @param velY The final vertical velocity of the gesture, in pixels/second. + */ + abstract fun onUp( + v: View, + ev: MotionEvent, + viewInitialX: Float, + viewInitialY: Float, + dx: Float, + dy: Float, + velX: Float, + velY: Float + ) + + /** The raw coordinates of the last ACTION_DOWN event. */ + private val touchDown = PointF() + + /** The coordinates of the view, at the time of the last ACTION_DOWN event. */ + private val viewPositionOnTouchDown = PointF() + + private val velocityTracker = VelocityTracker.obtain() + + private var touchSlop: Int = -1 + private var movedEnough = false + + private var performedLongClick = false + + @Suppress("UNCHECKED_CAST") + override fun onTouch(v: View, ev: MotionEvent): Boolean { + addMovement(ev) + + val dx = ev.rawX - touchDown.x + val dy = ev.rawY - touchDown.y + + when (ev.action) { + MotionEvent.ACTION_DOWN -> { + if (!onDown(v, ev)) { + return false + } + + // Grab the touch slop, it might have changed if the config changed since the + // last gesture. + touchSlop = ViewConfiguration.get(v.context).scaledTouchSlop + + touchDown.set(ev.rawX, ev.rawY) + viewPositionOnTouchDown.set(v.translationX, v.translationY) + + performedLongClick = false + v.handler.postDelayed({ + if (v.isLongClickable) { + performedLongClick = v.performLongClick() + } + }, ViewConfiguration.getLongPressTimeout().toLong()) + } + + MotionEvent.ACTION_MOVE -> { + if (!movedEnough && hypot(dx, dy) > touchSlop && !performedLongClick) { + movedEnough = true + v.handler.removeCallbacksAndMessages(null) + } + + if (movedEnough) { + onMove(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y, dx, dy) + } + } + + MotionEvent.ACTION_UP -> { + if (movedEnough) { + velocityTracker.computeCurrentVelocity(1000 /* units */) + onUp(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y, dx, dy, + velocityTracker.xVelocity, velocityTracker.yVelocity) + } else if (!performedLongClick) { + v.performClick() + } else { + v.handler.removeCallbacksAndMessages(null) + } + + velocityTracker.clear() + movedEnough = false + } + } + + return true + } + + /** + * Adds a movement to the velocity tracker using raw screen coordinates. + */ + private fun addMovement(event: MotionEvent) { + val deltaX = event.rawX - event.x + val deltaY = event.rawY - event.y + event.offsetLocation(deltaX, deltaY) + velocityTracker.addMovement(event) + event.offsetLocation(-deltaX, -deltaY) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt new file mode 100644 index 000000000000..04c4dfb9b08d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt @@ -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.bubbles + +import android.content.Context +import android.graphics.Color +import android.graphics.PointF +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import com.android.internal.util.ContrastColorUtil +import com.android.wm.shell.R +import com.android.wm.shell.animation.Interpolators + +/** + * User education view to highlight the collapsed stack of bubbles. + * Shown only the first time a user taps the stack. + */ +class StackEducationView constructor(context: Context) : LinearLayout(context) { + + private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleStackEducationView" + else BubbleDebugConfig.TAG_BUBBLES + + private val ANIMATE_DURATION: Long = 200 + private val ANIMATE_DURATION_SHORT: Long = 40 + + private val view by lazy { findViewById<View>(R.id.stack_education_layout) } + private val titleTextView by lazy { findViewById<TextView>(R.id.stack_education_title) } + private val descTextView by lazy { findViewById<TextView>(R.id.stack_education_description) } + + private var isHiding = false + + init { + LayoutInflater.from(context).inflate(R.layout.bubble_stack_user_education, this) + + visibility = View.GONE + elevation = resources.getDimensionPixelSize(R.dimen.bubble_elevation).toFloat() + + // BubbleStackView forces LTR by default + // since most of Bubble UI direction depends on positioning by the user. + // This view actually lays out differently in RTL, so we set layout LOCALE here. + layoutDirection = View.LAYOUT_DIRECTION_LOCALE + } + + override fun setLayoutDirection(layoutDirection: Int) { + super.setLayoutDirection(layoutDirection) + setDrawableDirection() + } + + override fun onFinishInflate() { + super.onFinishInflate() + layoutDirection = resources.configuration.layoutDirection + setTextColor() + } + + private fun setTextColor() { + val ta = mContext.obtainStyledAttributes(intArrayOf(android.R.attr.colorAccent, + android.R.attr.textColorPrimaryInverse)) + val bgColor = ta.getColor(0 /* index */, Color.BLACK) + var textColor = ta.getColor(1 /* index */, Color.WHITE) + ta.recycle() + textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true) + titleTextView.setTextColor(textColor) + descTextView.setTextColor(textColor) + } + + private fun setDrawableDirection() { + view.setBackgroundResource( + if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR) + R.drawable.bubble_stack_user_education_bg + else R.drawable.bubble_stack_user_education_bg_rtl) + } + + /** + * If necessary, shows the user education view for the bubble stack. This appears the first + * time a user taps on a bubble. + * + * @return true if user education was shown, false otherwise. + */ + fun show(stackPosition: PointF): Boolean { + if (visibility == VISIBLE) return false + + setAlpha(0f) + setVisibility(View.VISIBLE) + post { + with(view) { + val bubbleSize = context.resources.getDimensionPixelSize( + R.dimen.individual_bubble_size) + translationY = stackPosition.y + bubbleSize / 2 - getHeight() / 2 + } + animate() + .setDuration(ANIMATE_DURATION) + .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) + .alpha(1f) + } + setShouldShow(false) + return true + } + + /** + * If necessary, hides the stack education view. + * + * @param fromExpansion if true this indicates the hide is happening due to the bubble being + * expanded, false if due to a touch outside of the bubble stack. + */ + fun hide(fromExpansion: Boolean) { + if (visibility != VISIBLE || isHiding) return + + animate() + .alpha(0f) + .setDuration(if (fromExpansion) ANIMATE_DURATION_SHORT else ANIMATE_DURATION) + .withEndAction { visibility = GONE } + } + + private fun setShouldShow(shouldShow: Boolean) { + context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE) + .edit().putBoolean(PREF_STACK_EDUCATION, !shouldShow).apply() + } +} + +const val PREF_STACK_EDUCATION: String = "HasSeenBubblesOnboarding"
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/AnimatableScaleMatrix.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/AnimatableScaleMatrix.java new file mode 100644 index 000000000000..2612b81aae00 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/AnimatableScaleMatrix.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.bubbles.animation; + +import android.graphics.Matrix; + +import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.dynamicanimation.animation.FloatPropertyCompat; + +/** + * Matrix whose scale properties can be animated using physics animations, via the {@link #SCALE_X} + * and {@link #SCALE_Y} FloatProperties. + * + * This is useful when you need to perform a scale animation with a pivot point, since pivot points + * are not supported by standard View scale operations but are supported by matrices. + * + * NOTE: DynamicAnimation assumes that all custom properties are denominated in pixels, and thus + * considers 1 to be the smallest user-visible change for custom properties. This means that if you + * animate {@link #SCALE_X} and {@link #SCALE_Y} to 3f, for example, the animation would have only + * three frames. + * + * To work around this, whenever animating to a desired scale value, animate to the value returned + * by {@link #getAnimatableValueForScaleFactor} instead. The SCALE_X and SCALE_Y properties will + * convert that (larger) value into the appropriate scale factor when scaling the matrix. + */ +public class AnimatableScaleMatrix extends Matrix { + + /** + * The X value of the scale. + * + * NOTE: This must be set or animated to the value returned by + * {@link #getAnimatableValueForScaleFactor}, not the desired scale factor itself. + */ + public static final FloatPropertyCompat<AnimatableScaleMatrix> SCALE_X = + new FloatPropertyCompat<AnimatableScaleMatrix>("matrixScaleX") { + @Override + public float getValue(AnimatableScaleMatrix object) { + return getAnimatableValueForScaleFactor(object.mScaleX); + } + + @Override + public void setValue(AnimatableScaleMatrix object, float value) { + object.setScaleX(value * DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE); + } + }; + + /** + * The Y value of the scale. + * + * NOTE: This must be set or animated to the value returned by + * {@link #getAnimatableValueForScaleFactor}, not the desired scale factor itself. + */ + public static final FloatPropertyCompat<AnimatableScaleMatrix> SCALE_Y = + new FloatPropertyCompat<AnimatableScaleMatrix>("matrixScaleY") { + @Override + public float getValue(AnimatableScaleMatrix object) { + return getAnimatableValueForScaleFactor(object.mScaleY); + } + + @Override + public void setValue(AnimatableScaleMatrix object, float value) { + object.setScaleY(value * DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE); + } + }; + + private float mScaleX = 1f; + private float mScaleY = 1f; + + private float mPivotX = 0f; + private float mPivotY = 0f; + + /** + * Return the value to animate SCALE_X or SCALE_Y to in order to achieve the desired scale + * factor. + */ + public static float getAnimatableValueForScaleFactor(float scale) { + return scale * (1f / DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE); + } + + @Override + public void setScale(float sx, float sy, float px, float py) { + mScaleX = sx; + mScaleY = sy; + mPivotX = px; + mPivotY = py; + super.setScale(mScaleX, mScaleY, mPivotX, mPivotY); + } + + public void setScaleX(float scaleX) { + mScaleX = scaleX; + super.setScale(mScaleX, mScaleY, mPivotX, mPivotY); + } + + public void setScaleY(float scaleY) { + mScaleY = scaleY; + super.setScale(mScaleX, mScaleY, mPivotX, mPivotY); + } + + public void setPivotX(float pivotX) { + mPivotX = pivotX; + super.setScale(mScaleX, mScaleY, mPivotX, mPivotY); + } + + public void setPivotY(float pivotY) { + mPivotY = pivotY; + super.setScale(mScaleX, mScaleY, mPivotX, mPivotY); + } + + public float getScaleX() { + return mScaleX; + } + + public float getScaleY() { + return mScaleY; + } + + public float getPivotX() { + return mPivotX; + } + + public float getPivotY() { + return mPivotY; + } + + @Override + public boolean equals(Object obj) { + // Use object equality to allow this matrix to be used as a map key (which is required for + // PhysicsAnimator's animator caching). + return obj == this; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java new file mode 100644 index 000000000000..18aaa9677be6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java @@ -0,0 +1,645 @@ +/* + * 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.bubbles.animation; + +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Path; +import android.graphics.PointF; +import android.graphics.Rect; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.dynamicanimation.animation.SpringForce; + +import com.android.wm.shell.R; +import com.android.wm.shell.animation.Interpolators; +import com.android.wm.shell.animation.PhysicsAnimator; +import com.android.wm.shell.bubbles.BubblePositioner; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; + +import com.google.android.collect.Sets; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.Set; + +/** + * Animation controller for bubbles when they're in their expanded state, or animating to/from the + * expanded state. This controls the expansion animation as well as bubbles 'dragging out' to be + * dismissed. + */ +public class ExpandedAnimationController + extends PhysicsAnimationLayout.PhysicsAnimationController { + + /** + * How much to translate the bubbles when they're animating in/out. This value is multiplied by + * the bubble size. + */ + private static final int ANIMATE_TRANSLATION_FACTOR = 4; + + /** Duration of the expand/collapse target path animation. */ + public static final int EXPAND_COLLAPSE_TARGET_ANIM_DURATION = 175; + + /** Damping ratio for expand/collapse spring. */ + private static final float DAMPING_RATIO_MEDIUM_LOW_BOUNCY = 0.65f; + + /** Stiffness for the expand/collapse path-following animation. */ + private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 1000; + + /** What percentage of the screen to use when centering the bubbles in landscape. */ + private static final float CENTER_BUBBLES_LANDSCAPE_PERCENT = 0.66f; + + /** + * Velocity required to dismiss an individual bubble without dragging it into the dismiss + * target. + */ + private static final float FLING_TO_DISMISS_MIN_VELOCITY = 6000f; + + private final PhysicsAnimator.SpringConfig mAnimateOutSpringConfig = + new PhysicsAnimator.SpringConfig( + EXPAND_COLLAPSE_ANIM_STIFFNESS, SpringForce.DAMPING_RATIO_NO_BOUNCY); + + /** Horizontal offset between bubbles, which we need to know to re-stack them. */ + private float mStackOffsetPx; + /** Space between status bar and bubbles in the expanded state. */ + private float mBubblePaddingTop; + /** Size of each bubble. */ + private float mBubbleSizePx; + /** Max number of bubbles shown in row above expanded view. */ + private int mBubblesMaxRendered; + /** Max amount of space to have between bubbles when expanded. */ + private int mBubblesMaxSpace; + /** Amount of space between the bubbles when expanded. */ + private float mSpaceBetweenBubbles; + /** Whether the expand / collapse animation is running. */ + private boolean mAnimatingExpand = false; + + /** + * Whether we are animating other Bubbles UI elements out in preparation for a call to + * {@link #collapseBackToStack}. If true, we won't animate bubbles in response to adds or + * reorders. + */ + private boolean mPreparingToCollapse = false; + + private boolean mAnimatingCollapse = false; + @Nullable + private Runnable mAfterExpand; + private Runnable mAfterCollapse; + private PointF mCollapsePoint; + + /** + * Whether the dragged out bubble is springing towards the touch point, rather than using the + * default behavior of moving directly to the touch point. + * + * This happens when the user's finger exits the dismiss area while the bubble is magnetized to + * the center. Since the touch point differs from the bubble location, we need to animate the + * bubble back to the touch point to avoid a jarring instant location change from the center of + * the target to the touch point just outside the target bounds. + */ + private boolean mSpringingBubbleToTouch = false; + + /** + * Whether to spring the bubble to the next touch event coordinates. This is used to animate the + * bubble out of the magnetic dismiss target to the touch location. + * + * Once it 'catches up' and the animation ends, we'll revert to moving it directly. + */ + private boolean mSpringToTouchOnNextMotionEvent = false; + + /** The bubble currently being dragged out of the row (to potentially be dismissed). */ + private MagnetizedObject<View> mMagnetizedBubbleDraggingOut; + + private int mExpandedViewPadding; + + /** + * Callback to run whenever any bubble is animated out. The BubbleStackView will check if the + * end of this animation means we have no bubbles left, and notify the BubbleController. + */ + private Runnable mOnBubbleAnimatedOutAction; + + private BubblePositioner mPositioner; + + public ExpandedAnimationController(BubblePositioner positioner, int expandedViewPadding, + Runnable onBubbleAnimatedOutAction) { + mPositioner = positioner; + updateResources(); + mExpandedViewPadding = expandedViewPadding; + mOnBubbleAnimatedOutAction = onBubbleAnimatedOutAction; + } + + /** + * Whether the individual bubble has been dragged out of the row of bubbles far enough to cause + * the rest of the bubbles to animate to fill the gap. + */ + private boolean mBubbleDraggedOutEnough = false; + + /** End action to run when the lead bubble's expansion animation completes. */ + @Nullable + private Runnable mLeadBubbleEndAction; + + /** + * Animates expanding the bubbles into a row along the top of the screen, optionally running an + * end action when the entire animation completes, and an end action when the lead bubble's + * animation ends. + */ + public void expandFromStack( + @Nullable Runnable after, @Nullable Runnable leadBubbleEndAction) { + mPreparingToCollapse = false; + mAnimatingCollapse = false; + mAnimatingExpand = true; + mAfterExpand = after; + mLeadBubbleEndAction = leadBubbleEndAction; + + startOrUpdatePathAnimation(true /* expanding */); + } + + /** + * Animates expanding the bubbles into a row along the top of the screen. + */ + public void expandFromStack(@Nullable Runnable after) { + expandFromStack(after, null /* leadBubbleEndAction */); + } + + /** + * Sets that we're animating the stack collapsed, but haven't yet called + * {@link #collapseBackToStack}. This will temporarily suspend animations for bubbles that are + * added or re-ordered, since the upcoming collapse animation will handle positioning those + * bubbles in the collapsed stack. + */ + public void notifyPreparingToCollapse() { + mPreparingToCollapse = true; + } + + /** Animate collapsing the bubbles back to their stacked position. */ + public void collapseBackToStack(PointF collapsePoint, Runnable after) { + mAnimatingExpand = false; + mPreparingToCollapse = false; + mAnimatingCollapse = true; + mAfterCollapse = after; + mCollapsePoint = collapsePoint; + + startOrUpdatePathAnimation(false /* expanding */); + } + + /** + * Update effective screen width based on current orientation. + */ + public void updateResources() { + if (mLayout == null) { + return; + } + Resources res = mLayout.getContext().getResources(); + mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); + mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); + mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); + mBubbleSizePx = mPositioner.getBubbleSize(); + mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered); + mBubblesMaxSpace = res.getDimensionPixelSize(R.dimen.bubble_max_spacing); + final float availableSpace = mPositioner.isLandscape() + ? mPositioner.getAvailableRect().height() + : mPositioner.getAvailableRect().width(); + final float spaceForMaxBubbles = (mExpandedViewPadding * 2) + + (mBubblesMaxRendered + 1) * mBubbleSizePx; + float spaceBetweenBubbles = + (availableSpace - spaceForMaxBubbles) / mBubblesMaxRendered; + mSpaceBetweenBubbles = Math.min(spaceBetweenBubbles, mBubblesMaxSpace); + } + + /** + * Animates the bubbles along a curved path, either to expand them along the top or collapse + * them back into a stack. + */ + private void startOrUpdatePathAnimation(boolean expanding) { + Runnable after; + + if (expanding) { + after = () -> { + mAnimatingExpand = false; + + if (mAfterExpand != null) { + mAfterExpand.run(); + } + + mAfterExpand = null; + + // Update bubble positions in case any bubbles were added or removed during the + // expansion animation. + updateBubblePositions(); + }; + } else { + after = () -> { + mAnimatingCollapse = false; + + if (mAfterCollapse != null) { + mAfterCollapse.run(); + } + + mAfterCollapse = null; + }; + } + + // Animate each bubble individually, since each path will end in a different spot. + animationsForChildrenFromIndex(0, (index, animation) -> { + final View bubble = mLayout.getChildAt(index); + + // Start a path at the bubble's current position. + final Path path = new Path(); + path.moveTo(bubble.getTranslationX(), bubble.getTranslationY()); + + final float expandedY = mPositioner.showBubblesVertically() + ? getBubbleXOrYForOrientation(index) + : getExpandedY(); + if (expanding) { + // If we're expanding, first draw a line from the bubble's current position to the + // top of the screen. + path.lineTo(bubble.getTranslationX(), expandedY); + // Then, draw a line across the screen to the bubble's resting position. + if (mPositioner.showBubblesVertically()) { + Rect availableRect = mPositioner.getAvailableRect(); + boolean onLeft = mCollapsePoint != null + && mCollapsePoint.x < (availableRect.width() / 2f); + float translationX = onLeft + ? availableRect.left + mExpandedViewPadding + : availableRect.right - mBubbleSizePx - mExpandedViewPadding; + path.lineTo(translationX, getBubbleXOrYForOrientation(index)); + } else { + path.lineTo(getBubbleXOrYForOrientation(index), expandedY); + } + } else { + final float stackedX = mCollapsePoint.x; + + // If we're collapsing, draw a line from the bubble's current position to the side + // of the screen where the bubble will be stacked. + path.lineTo(stackedX, expandedY); + + // Then, draw a line down to the stack position. + path.lineTo(stackedX, mCollapsePoint.y + index * mStackOffsetPx); + } + + // The lead bubble should be the bubble with the longest distance to travel when we're + // expanding, and the bubble with the shortest distance to travel when we're collapsing. + // During expansion from the left side, the last bubble has to travel to the far right + // side, so we have it lead and 'pull' the rest of the bubbles into place. From the + // right side, the first bubble is traveling to the top left, so it leads. During + // collapse to the left, the first bubble has the shortest travel time back to the stack + // position, so it leads (and vice versa). + final boolean firstBubbleLeads = + (expanding && !mLayout.isFirstChildXLeftOfCenter(bubble.getTranslationX())) + || (!expanding && mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x)); + final int startDelay = firstBubbleLeads + ? (index * 10) + : ((mLayout.getChildCount() - index) * 10); + + final boolean isLeadBubble = + (firstBubbleLeads && index == 0) + || (!firstBubbleLeads && index == mLayout.getChildCount() - 1); + + animation + .followAnimatedTargetAlongPath( + path, + EXPAND_COLLAPSE_TARGET_ANIM_DURATION /* targetAnimDuration */, + Interpolators.LINEAR /* targetAnimInterpolator */, + isLeadBubble ? mLeadBubbleEndAction : null /* endAction */, + () -> mLeadBubbleEndAction = null /* endAction */) + .withStartDelay(startDelay) + .withStiffness(EXPAND_COLLAPSE_ANIM_STIFFNESS); + }).startAll(after); + } + + /** Notifies the controller that the dragged-out bubble was unstuck from the magnetic target. */ + public void onUnstuckFromTarget() { + mSpringToTouchOnNextMotionEvent = true; + } + + /** + * Prepares the given bubble view to be dragged out, using the provided magnetic target and + * listener. + */ + public void prepareForBubbleDrag( + View bubble, + MagnetizedObject.MagneticTarget target, + MagnetizedObject.MagnetListener listener) { + mLayout.cancelAnimationsOnView(bubble); + + bubble.setTranslationZ(Short.MAX_VALUE); + mMagnetizedBubbleDraggingOut = new MagnetizedObject<View>( + mLayout.getContext(), bubble, + DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) { + @Override + public float getWidth(@NonNull View underlyingObject) { + return mBubbleSizePx; + } + + @Override + public float getHeight(@NonNull View underlyingObject) { + return mBubbleSizePx; + } + + @Override + public void getLocationOnScreen(@NonNull View underlyingObject, @NonNull int[] loc) { + loc[0] = (int) bubble.getTranslationX(); + loc[1] = (int) bubble.getTranslationY(); + } + }; + mMagnetizedBubbleDraggingOut.addTarget(target); + mMagnetizedBubbleDraggingOut.setMagnetListener(listener); + mMagnetizedBubbleDraggingOut.setHapticsEnabled(true); + mMagnetizedBubbleDraggingOut.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY); + } + + private void springBubbleTo(View bubble, float x, float y) { + animationForChild(bubble) + .translationX(x) + .translationY(y) + .withStiffness(SpringForce.STIFFNESS_HIGH) + .start(); + } + + /** + * Drags an individual bubble to the given coordinates. Bubbles to the right will animate to + * take its place once it's dragged out of the row of bubbles, and animate out of the way if the + * bubble is dragged back into the row. + */ + public void dragBubbleOut(View bubbleView, float x, float y) { + if (mSpringToTouchOnNextMotionEvent) { + springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y); + mSpringToTouchOnNextMotionEvent = false; + mSpringingBubbleToTouch = true; + } else if (mSpringingBubbleToTouch) { + if (mLayout.arePropertiesAnimatingOnView( + bubbleView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)) { + springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y); + } else { + mSpringingBubbleToTouch = false; + } + } + + if (!mSpringingBubbleToTouch && !mMagnetizedBubbleDraggingOut.getObjectStuckToTarget()) { + bubbleView.setTranslationX(x); + bubbleView.setTranslationY(y); + } + + final boolean draggedOutEnough = + y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx; + if (draggedOutEnough != mBubbleDraggedOutEnough) { + updateBubblePositions(); + mBubbleDraggedOutEnough = draggedOutEnough; + } + } + + /** Plays a dismiss animation on the dragged out bubble. */ + public void dismissDraggedOutBubble(View bubble, float translationYBy, Runnable after) { + if (bubble == null) { + return; + } + animationForChild(bubble) + .withStiffness(SpringForce.STIFFNESS_HIGH) + .scaleX(0f) + .scaleY(0f) + .translationY(bubble.getTranslationY() + translationYBy) + .alpha(0f, after) + .start(); + + updateBubblePositions(); + } + + @Nullable + public View getDraggedOutBubble() { + return mMagnetizedBubbleDraggingOut == null + ? null + : mMagnetizedBubbleDraggingOut.getUnderlyingObject(); + } + + /** Returns the MagnetizedObject instance for the dragging-out bubble. */ + public MagnetizedObject<View> getMagnetizedBubbleDraggingOut() { + return mMagnetizedBubbleDraggingOut; + } + + /** + * Snaps a bubble back to its position within the bubble row, and animates the rest of the + * bubbles to accommodate it if it was previously dragged out past the threshold. + */ + public void snapBubbleBack(View bubbleView, float velX, float velY) { + if (mLayout == null) { + return; + } + final int index = mLayout.indexOfChild(bubbleView); + + animationForChildAtIndex(index) + .position(getBubbleXOrYForOrientation(index), getExpandedY()) + .withPositionStartVelocities(velX, velY) + .start(() -> bubbleView.setTranslationZ(0f) /* after */); + + mMagnetizedBubbleDraggingOut = null; + + updateBubblePositions(); + } + + /** Resets bubble drag out gesture flags. */ + public void onGestureFinished() { + mBubbleDraggedOutEnough = false; + mMagnetizedBubbleDraggingOut = null; + updateBubblePositions(); + } + + /** + * Animates the bubbles to {@link #getExpandedY()} position. Used in response to IME showing. + */ + public void updateYPosition(Runnable after) { + if (mLayout == null) return; + animationsForChildrenFromIndex( + 0, (i, anim) -> anim.translationY(getExpandedY())).startAll(after); + } + + /** The Y value of the row of expanded bubbles. */ + public float getExpandedY() { + return mPositioner.getAvailableRect().top + mBubblePaddingTop; + } + + /** Description of current animation controller state. */ + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("ExpandedAnimationController state:"); + pw.print(" isActive: "); pw.println(isActiveController()); + pw.print(" animatingExpand: "); pw.println(mAnimatingExpand); + pw.print(" animatingCollapse: "); pw.println(mAnimatingCollapse); + pw.print(" springingBubble: "); pw.println(mSpringingBubbleToTouch); + } + + @Override + void onActiveControllerForLayout(PhysicsAnimationLayout layout) { + updateResources(); + + // Ensure that all child views are at 1x scale, and visible, in case they were animating + // in. + mLayout.setVisibility(View.VISIBLE); + animationsForChildrenFromIndex(0 /* startIndex */, (index, animation) -> + animation.scaleX(1f).scaleY(1f).alpha(1f)).startAll(); + } + + @Override + Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { + return Sets.newHashSet( + DynamicAnimation.TRANSLATION_X, + DynamicAnimation.TRANSLATION_Y, + DynamicAnimation.SCALE_X, + DynamicAnimation.SCALE_Y, + DynamicAnimation.ALPHA); + } + + @Override + int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) { + return NONE; + } + + @Override + float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) { + return 0; + } + + @Override + SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) { + return new SpringForce() + .setDampingRatio(DAMPING_RATIO_MEDIUM_LOW_BOUNCY) + .setStiffness(SpringForce.STIFFNESS_LOW); + } + + @Override + void onChildAdded(View child, int index) { + // If a bubble is added while the expand/collapse animations are playing, update the + // animation to include the new bubble. + if (mAnimatingExpand) { + startOrUpdatePathAnimation(true /* expanding */); + } else if (mAnimatingCollapse) { + startOrUpdatePathAnimation(false /* expanding */); + } else { + child.setTranslationX(getBubbleXOrYForOrientation(index)); + + // If we're preparing to collapse, don't start animations since the collapse animation + // will take over and animate the new bubble into the correct (stacked) position. + if (!mPreparingToCollapse) { + animationForChild(child) + .translationY( + getExpandedY() + - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */ + getExpandedY() /* to */) + .start(); + updateBubblePositions(); + } + } + } + + @Override + void onChildRemoved(View child, int index, Runnable finishRemoval) { + // If we're removing the dragged-out bubble, that means it got dismissed. + if (child.equals(getDraggedOutBubble())) { + mMagnetizedBubbleDraggingOut = null; + finishRemoval.run(); + mOnBubbleAnimatedOutAction.run(); + } else { + PhysicsAnimator.getInstance(child) + .spring(DynamicAnimation.ALPHA, 0f) + .spring(DynamicAnimation.SCALE_X, 0f, mAnimateOutSpringConfig) + .spring(DynamicAnimation.SCALE_Y, 0f, mAnimateOutSpringConfig) + .withEndActions(finishRemoval, mOnBubbleAnimatedOutAction) + .start(); + } + + // Animate all the other bubbles to their new positions sans this bubble. + updateBubblePositions(); + } + + @Override + void onChildReordered(View child, int oldIndex, int newIndex) { + if (mPreparingToCollapse) { + // If a re-order is received while we're preparing to collapse, ignore it. Once started, + // the collapse animation will animate all of the bubbles to their correct (stacked) + // position. + return; + } + + if (mAnimatingCollapse) { + // If a re-order is received during collapse, update the animation so that the bubbles + // end up in the correct (stacked) position. + startOrUpdatePathAnimation(false /* expanding */); + } else { + // Otherwise, animate the bubbles around to reflect their new order. + updateBubblePositions(); + } + } + + private void updateBubblePositions() { + if (mAnimatingExpand || mAnimatingCollapse) { + return; + } + + for (int i = 0; i < mLayout.getChildCount(); i++) { + final View bubble = mLayout.getChildAt(i); + + // Don't animate the dragging out bubble, or it'll jump around while being dragged. It + // will be snapped to the correct X value after the drag (if it's not dismissed). + if (bubble.equals(getDraggedOutBubble())) { + return; + } + + if (mPositioner.showBubblesVertically()) { + Rect availableRect = mPositioner.getAvailableRect(); + boolean onLeft = mCollapsePoint != null + && mCollapsePoint.x < (availableRect.width() / 2f); + animationForChild(bubble) + .translationX(onLeft + ? availableRect.left + mExpandedViewPadding + : availableRect.right - mBubbleSizePx - mExpandedViewPadding) + .translationY(getBubbleXOrYForOrientation(i)) + .start(); + } else { + animationForChild(bubble) + .translationX(getBubbleXOrYForOrientation(i)) + .translationY(getExpandedY()) + .start(); + } + } + } + + /** + * When bubbles are expanded in portrait, they display at the top of the screen in a horizontal + * row. When in landscape, they show at the left or right side in a vertical row. This method + * accounts for screen orientation and will return an x or y value for the position of the + * bubble in the row. + * + * @param index Bubble index in row. + * @return the y position of the bubble if {@link Configuration#ORIENTATION_LANDSCAPE} and the + * x position if {@link Configuration#ORIENTATION_PORTRAIT}. + */ + public float getBubbleXOrYForOrientation(int index) { + if (mLayout == null) { + return 0; + } + final float positionInBar = index * (mBubbleSizePx + mSpaceBetweenBubbles); + Rect availableRect = mPositioner.getAvailableRect(); + final boolean isLandscape = mPositioner.showBubblesVertically(); + final float expandedStackSize = (mLayout.getChildCount() * mBubbleSizePx) + + ((mLayout.getChildCount() - 1) * mSpaceBetweenBubbles); + final float centerPosition = isLandscape + ? availableRect.centerY() + : availableRect.centerX(); + final float rowStart = centerPosition - (expandedStackSize / 2f); + return rowStart + positionInBar; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/OneTimeEndListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/OneTimeEndListener.java new file mode 100644 index 000000000000..37355c41810c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/OneTimeEndListener.java @@ -0,0 +1,34 @@ +/* + * 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.bubbles.animation; + +import androidx.dynamicanimation.animation.DynamicAnimation; + +/** + * End listener that removes itself from its animation when called for the first time. Useful since + * anonymous OnAnimationEndListener instances can't pass themselves to + * {@link DynamicAnimation#removeEndListener}, but can call through to this superclass + * implementation. + */ +public class OneTimeEndListener implements DynamicAnimation.OnAnimationEndListener { + + @Override + public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value, + float velocity) { + animation.removeEndListener(this); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java new file mode 100644 index 000000000000..0618d5d5f213 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java @@ -0,0 +1,1163 @@ +/* + * 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.bubbles.animation; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.content.Context; +import android.graphics.Path; +import android.graphics.PointF; +import android.util.FloatProperty; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewPropertyAnimator; +import android.widget.FrameLayout; + +import androidx.annotation.Nullable; +import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.dynamicanimation.animation.SpringAnimation; +import androidx.dynamicanimation.animation.SpringForce; + +import com.android.wm.shell.R; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Layout that constructs physics-based animations for each of its children, which behave according + * to settings provided by a {@link PhysicsAnimationController} instance. + * + * See physics-animation-layout.md. + */ +public class PhysicsAnimationLayout extends FrameLayout { + private static final String TAG = "Bubbs.PAL"; + + /** + * Controls the construction, configuration, and use of the physics animations supplied by this + * layout. + */ + abstract static class PhysicsAnimationController { + + /** Configures a given {@link PhysicsPropertyAnimator} for a view at the given index. */ + interface ChildAnimationConfigurator { + + /** + * Called to configure the animator for the view at the given index. + * + * This method should make use of methods such as + * {@link PhysicsPropertyAnimator#translationX} and + * {@link PhysicsPropertyAnimator#withStartDelay} to configure the animation. + * + * Implementations should not call {@link PhysicsPropertyAnimator#start}, this will + * happen elsewhere after configuration is complete. + */ + void configureAnimationForChildAtIndex(int index, PhysicsPropertyAnimator animation); + } + + /** + * Returned by {@link #animationsForChildrenFromIndex} to allow starting multiple animations + * on multiple child views at the same time. + */ + interface MultiAnimationStarter { + + /** + * Start all animations and call the given end actions once all animations have + * completed. + */ + void startAll(Runnable... endActions); + } + + /** + * Constant to return from {@link #getNextAnimationInChain} if the animation should not be + * chained at all. + */ + protected static final int NONE = -1; + + /** Set of properties for which the layout should construct physics animations. */ + abstract Set<DynamicAnimation.ViewProperty> getAnimatedProperties(); + + /** + * Returns the index of the next animation after the given index in the animation chain, or + * {@link #NONE} if it should not be chained, or if the chain should end at the given index. + * + * If a next index is returned, an update listener will be added to the animation at the + * given index that dispatches value updates to the animation at the next index. This + * creates a 'following' effect. + * + * Typical implementations of this method will return either index + 1, or index - 1, to + * create forward or backward chains between adjacent child views, but this is not required. + */ + abstract int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index); + + /** + * Offsets to be added to the value that chained animations of the given property dispatch + * to subsequent child animations. + * + * This is used for things like maintaining the 'stack' effect in Bubbles, where bubbles + * stack off to the left or right side slightly. + */ + abstract float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property); + + /** + * Returns the SpringForce to be used for the given child view's property animation. Despite + * these usually being similar or identical across properties and views, {@link SpringForce} + * also contains the SpringAnimation's final position, so we have to construct a new one for + * each animation rather than using a constant. + */ + abstract SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view); + + /** + * Called when a new child is added at the specified index. Controllers can use this + * opportunity to animate in the new view. + */ + abstract void onChildAdded(View child, int index); + + /** + * Called with a child view that has been removed from the layout, from the given index. The + * passed view has been removed from the layout and added back as a transient view, which + * renders normally, but is not part of the normal view hierarchy and will not be considered + * by getChildAt() and getChildCount(). + * + * The controller can perform animations on the child (either manually, or by using + * {@link #animationForChild(View)}), and then call finishRemoval when complete. + * + * finishRemoval must be called by implementations of this method, or transient views will + * never be removed. + */ + abstract void onChildRemoved(View child, int index, Runnable finishRemoval); + + /** Called when a child view has been reordered in the view hierachy. */ + abstract void onChildReordered(View child, int oldIndex, int newIndex); + + /** + * Called when the controller is set as the active animation controller for the given + * layout. Once active, the controller can start animations using the animator instances + * returned by {@link #animationForChild}. + * + * While all animations started by the previous controller will be cancelled, the new + * controller should not make any assumptions about the state of the layout or its children. + * Their translation, alpha, scale, etc. values may have been changed by the previous + * controller and should be reset here if relevant. + */ + abstract void onActiveControllerForLayout(PhysicsAnimationLayout layout); + + protected PhysicsAnimationLayout mLayout; + + PhysicsAnimationController() { } + + /** Whether this controller is the currently active controller for its associated layout. */ + protected boolean isActiveController() { + return mLayout != null && this == mLayout.mController; + } + + protected void setLayout(PhysicsAnimationLayout layout) { + this.mLayout = layout; + onActiveControllerForLayout(layout); + } + + protected PhysicsAnimationLayout getLayout() { + return mLayout; + } + + /** + * Returns a {@link PhysicsPropertyAnimator} instance for the given child view. + */ + protected PhysicsPropertyAnimator animationForChild(View child) { + PhysicsPropertyAnimator animator = + (PhysicsPropertyAnimator) child.getTag(R.id.physics_animator_tag); + + if (animator == null) { + animator = mLayout.new PhysicsPropertyAnimator(child); + child.setTag(R.id.physics_animator_tag, animator); + } + + animator.clearAnimator(); + animator.setAssociatedController(this); + + return animator; + } + + /** Returns a {@link PhysicsPropertyAnimator} instance for child at the given index. */ + protected PhysicsPropertyAnimator animationForChildAtIndex(int index) { + return animationForChild(mLayout.getChildAt(index)); + } + + /** + * Returns a {@link MultiAnimationStarter} whose startAll method will start the physics + * animations for all children from startIndex onward. The provided configurator will be + * called with each child's {@link PhysicsPropertyAnimator}, where it can set up each + * animation appropriately. + */ + protected MultiAnimationStarter animationsForChildrenFromIndex( + int startIndex, ChildAnimationConfigurator configurator) { + final Set<DynamicAnimation.ViewProperty> allAnimatedProperties = new HashSet<>(); + final List<PhysicsPropertyAnimator> allChildAnims = new ArrayList<>(); + + // Retrieve the animator for each child, ask the configurator to configure it, then save + // it and the properties it chose to animate. + for (int i = startIndex; i < mLayout.getChildCount(); i++) { + final PhysicsPropertyAnimator anim = animationForChildAtIndex(i); + configurator.configureAnimationForChildAtIndex(i, anim); + allAnimatedProperties.addAll(anim.getAnimatedProperties()); + allChildAnims.add(anim); + } + + // Return a MultiAnimationStarter that will start all of the child animations, and also + // add a multiple property end listener to the layout that will call the end action + // provided to startAll() once all animations on the animated properties complete. + return (endActions) -> { + final Runnable runAllEndActions = () -> { + for (Runnable action : endActions) { + action.run(); + } + }; + + // If there aren't any children to animate, just run the end actions. + if (mLayout.getChildCount() == 0) { + runAllEndActions.run(); + return; + } + + if (endActions != null) { + setEndActionForMultipleProperties( + runAllEndActions, + allAnimatedProperties.toArray( + new DynamicAnimation.ViewProperty[0])); + } + + for (PhysicsPropertyAnimator childAnim : allChildAnims) { + childAnim.start(); + } + }; + } + + /** + * Sets an end action that will be run when all child animations for a given property have + * stopped running. + */ + protected void setEndActionForProperty( + Runnable action, DynamicAnimation.ViewProperty property) { + mLayout.mEndActionForProperty.put(property, action); + } + + /** + * Sets an end action that will be run when all child animations for all of the given + * properties have stopped running. + */ + protected void setEndActionForMultipleProperties( + Runnable action, DynamicAnimation.ViewProperty... properties) { + final Runnable checkIfAllFinished = () -> { + if (!mLayout.arePropertiesAnimating(properties)) { + action.run(); + + for (DynamicAnimation.ViewProperty property : properties) { + removeEndActionForProperty(property); + } + } + }; + + for (DynamicAnimation.ViewProperty property : properties) { + setEndActionForProperty(checkIfAllFinished, property); + } + } + + /** + * Removes the end listener that would have been called when all child animations for a + * given property stopped running. + */ + protected void removeEndActionForProperty(DynamicAnimation.ViewProperty property) { + mLayout.mEndActionForProperty.remove(property); + } + } + + /** + * End actions that are called when every child's animation of the given property has finished. + */ + protected final HashMap<DynamicAnimation.ViewProperty, Runnable> mEndActionForProperty = + new HashMap<>(); + + /** The currently active animation controller. */ + @Nullable protected PhysicsAnimationController mController; + + public PhysicsAnimationLayout(Context context) { + super(context); + } + + /** + * Sets the animation controller and constructs or reconfigures the layout's physics animations + * to meet the controller's specifications. + */ + public void setActiveController(PhysicsAnimationController controller) { + cancelAllAnimations(); + mEndActionForProperty.clear(); + + this.mController = controller; + mController.setLayout(this); + + // Set up animations for this controller's animated properties. + for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { + setUpAnimationsForProperty(property); + } + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + addViewInternal(child, index, params, false /* isReorder */); + } + + @Override + public void removeView(View view) { + if (mController != null) { + final int index = indexOfChild(view); + + // Remove the view and add it back as a transient view so we can animate it out. + super.removeView(view); + addTransientView(view, index); + + // Tell the controller to animate this view out, and call the callback when it's + // finished. + mController.onChildRemoved(view, index, () -> { + // The controller says it's done with the transient view, cancel animations in case + // any are still running and then remove it. + cancelAnimationsOnView(view); + removeTransientView(view); + }); + } else { + // Without a controller, nobody will animate this view out, so it gets an unceremonious + // departure. + super.removeView(view); + } + } + + @Override + public void removeViewAt(int index) { + removeView(getChildAt(index)); + } + + /** Immediately re-orders the view to the given index. */ + public void reorderView(View view, int index) { + if (view == null) { + return; + } + final int oldIndex = indexOfChild(view); + + super.removeView(view); + addViewInternal(view, index, view.getLayoutParams(), true /* isReorder */); + + if (mController != null) { + mController.onChildReordered(view, oldIndex, index); + } + } + + /** Checks whether any animations of the given properties are still running. */ + public boolean arePropertiesAnimating(DynamicAnimation.ViewProperty... properties) { + for (int i = 0; i < getChildCount(); i++) { + if (arePropertiesAnimatingOnView(getChildAt(i), properties)) { + return true; + } + } + + return false; + } + + /** Checks whether any animations of the given properties are running on the given view. */ + public boolean arePropertiesAnimatingOnView( + View view, DynamicAnimation.ViewProperty... properties) { + final ObjectAnimator targetAnimator = getTargetAnimatorFromView(view); + for (DynamicAnimation.ViewProperty property : properties) { + final SpringAnimation animation = getSpringAnimationFromView(property, view); + if (animation != null && animation.isRunning()) { + return true; + } + + // If the target animator is running, its update listener will trigger the translation + // physics animations at some point. We should consider the translation properties to be + // be animating in this case, even if the physics animations haven't been started yet. + final boolean isTranslation = + property.equals(DynamicAnimation.TRANSLATION_X) + || property.equals(DynamicAnimation.TRANSLATION_Y); + if (isTranslation && targetAnimator != null && targetAnimator.isRunning()) { + return true; + } + } + + return false; + } + + /** Cancels all animations that are running on all child views, for all properties. */ + public void cancelAllAnimations() { + if (mController == null) { + return; + } + + cancelAllAnimationsOfProperties( + mController.getAnimatedProperties().toArray(new DynamicAnimation.ViewProperty[]{})); + } + + /** Cancels all animations that are running on all child views, for the given properties. */ + public void cancelAllAnimationsOfProperties(DynamicAnimation.ViewProperty... properties) { + if (mController == null) { + return; + } + + for (int i = 0; i < getChildCount(); i++) { + for (DynamicAnimation.ViewProperty property : properties) { + final DynamicAnimation anim = getSpringAnimationAtIndex(property, i); + if (anim != null) { + anim.cancel(); + } + } + final ViewPropertyAnimator anim = getViewPropertyAnimatorFromView(getChildAt(i)); + if (anim != null) { + anim.cancel(); + } + } + } + + /** Cancels all of the physics animations running on the given view. */ + public void cancelAnimationsOnView(View view) { + // If present, cancel the target animator so it doesn't restart the translation physics + // animations. + final ObjectAnimator targetAnimator = getTargetAnimatorFromView(view); + if (targetAnimator != null) { + targetAnimator.cancel(); + } + + // Cancel physics animations on the view. + for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { + final DynamicAnimation animationFromView = getSpringAnimationFromView(property, view); + if (animationFromView != null) { + animationFromView.cancel(); + } + } + } + + protected boolean isActiveController(PhysicsAnimationController controller) { + return mController == controller; + } + + /** Whether the first child would be left of center if translated to the given x value. */ + protected boolean isFirstChildXLeftOfCenter(float x) { + if (getChildCount() > 0) { + return x + (getChildAt(0).getWidth() / 2) < getWidth() / 2; + } else { + return false; // If there's no first child, really anything is correct, right? + } + } + + /** ViewProperty's toString is useless, this returns a readable name for debug logging. */ + protected static String getReadablePropertyName(DynamicAnimation.ViewProperty property) { + if (property.equals(DynamicAnimation.TRANSLATION_X)) { + return "TRANSLATION_X"; + } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) { + return "TRANSLATION_Y"; + } else if (property.equals(DynamicAnimation.SCALE_X)) { + return "SCALE_X"; + } else if (property.equals(DynamicAnimation.SCALE_Y)) { + return "SCALE_Y"; + } else if (property.equals(DynamicAnimation.ALPHA)) { + return "ALPHA"; + } else { + return "Unknown animation property."; + } + } + + /** + * Adds a view to the layout. If this addition is not the result of a call to + * {@link #reorderView}, this will also notify the controller via + * {@link PhysicsAnimationController#onChildAdded} and set up animations for the view. + */ + private void addViewInternal( + View child, int index, ViewGroup.LayoutParams params, boolean isReorder) { + super.addView(child, index, params); + + // Set up animations for the new view, if the controller is set. If it isn't set, we'll be + // setting up animations for all children when setActiveController is called. + if (mController != null && !isReorder) { + for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { + setUpAnimationForChild(property, child, index); + } + + mController.onChildAdded(child, index); + } + } + + /** + * Retrieves the animation of the given property from the view at the given index via the view + * tag system. + */ + @Nullable private SpringAnimation getSpringAnimationAtIndex( + DynamicAnimation.ViewProperty property, int index) { + return getSpringAnimationFromView(property, getChildAt(index)); + } + + /** + * Retrieves the spring animation of the given property from the view via the view tag system. + */ + @Nullable private SpringAnimation getSpringAnimationFromView( + DynamicAnimation.ViewProperty property, View view) { + return (SpringAnimation) view.getTag(getTagIdForProperty(property)); + } + + /** + * Retrieves the view property animation of the given property from the view via the view tag + * system. + */ + @Nullable private ViewPropertyAnimator getViewPropertyAnimatorFromView(View view) { + return (ViewPropertyAnimator) view.getTag(R.id.reorder_animator_tag); + } + + /** Retrieves the target animator from the view via the view tag system. */ + @Nullable private ObjectAnimator getTargetAnimatorFromView(View view) { + return (ObjectAnimator) view.getTag(R.id.target_animator_tag); + } + + /** Sets up SpringAnimations of the given property for each child view in the layout. */ + private void setUpAnimationsForProperty(DynamicAnimation.ViewProperty property) { + for (int i = 0; i < getChildCount(); i++) { + setUpAnimationForChild(property, getChildAt(i), i); + } + } + + /** Constructs a SpringAnimation of the given property for a child view. */ + private void setUpAnimationForChild( + DynamicAnimation.ViewProperty property, View child, int index) { + SpringAnimation newAnim = new SpringAnimation(child, property); + newAnim.addUpdateListener((animation, value, velocity) -> { + final int indexOfChild = indexOfChild(child); + final int nextAnimInChain = mController.getNextAnimationInChain(property, indexOfChild); + + if (nextAnimInChain == PhysicsAnimationController.NONE || indexOfChild < 0) { + return; + } + + final float offset = mController.getOffsetForChainedPropertyAnimation(property); + if (nextAnimInChain < getChildCount()) { + final SpringAnimation nextAnim = getSpringAnimationAtIndex( + property, nextAnimInChain); + if (nextAnim != null) { + nextAnim.animateToFinalPosition(value + offset); + } + } + }); + + newAnim.setSpring(mController.getSpringForce(property, child)); + newAnim.addEndListener(new AllAnimationsForPropertyFinishedEndListener(property)); + child.setTag(getTagIdForProperty(property), newAnim); + } + + /** Return a stable ID to use as a tag key for the given property's animations. */ + private int getTagIdForProperty(DynamicAnimation.ViewProperty property) { + if (property.equals(DynamicAnimation.TRANSLATION_X)) { + return R.id.translation_x_dynamicanimation_tag; + } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) { + return R.id.translation_y_dynamicanimation_tag; + } else if (property.equals(DynamicAnimation.SCALE_X)) { + return R.id.scale_x_dynamicanimation_tag; + } else if (property.equals(DynamicAnimation.SCALE_Y)) { + return R.id.scale_y_dynamicanimation_tag; + } else if (property.equals(DynamicAnimation.ALPHA)) { + return R.id.alpha_dynamicanimation_tag; + } + + return -1; + } + + /** + * End listener that is added to each individual DynamicAnimation, which dispatches to a single + * listener when every other animation of the given property is no longer running. + * + * This is required since chained DynamicAnimations can stop and start again due to changes in + * upstream animations. This means that adding an end listener to just the last animation is not + * sufficient. By firing only when every other animation on the property has stopped running, we + * ensure that no animation will be restarted after the single end listener is called. + */ + protected class AllAnimationsForPropertyFinishedEndListener + implements DynamicAnimation.OnAnimationEndListener { + private DynamicAnimation.ViewProperty mProperty; + + AllAnimationsForPropertyFinishedEndListener(DynamicAnimation.ViewProperty property) { + this.mProperty = property; + } + + @Override + public void onAnimationEnd( + DynamicAnimation anim, boolean canceled, float value, float velocity) { + if (!arePropertiesAnimating(mProperty)) { + if (mEndActionForProperty.containsKey(mProperty)) { + final Runnable callback = mEndActionForProperty.get(mProperty); + + if (callback != null) { + callback.run(); + } + } + } + } + } + + /** + * Animator class returned by {@link PhysicsAnimationController#animationForChild}, to allow + * controllers to animate child views using physics animations. + * + * See docs/physics-animation-layout.md for documentation and examples. + */ + protected class PhysicsPropertyAnimator { + /** The view whose properties this animator animates. */ + private View mView; + + /** Start velocity to use for all property animations. */ + private float mDefaultStartVelocity = -Float.MAX_VALUE; + + /** Start delay to use when start is called. */ + private long mStartDelay = 0; + + /** Damping ratio to use for the animations. */ + private float mDampingRatio = -1; + + /** Stiffness to use for the animations. */ + private float mStiffness = -1; + + /** End actions to call when animations for the given property complete. */ + private Map<DynamicAnimation.ViewProperty, Runnable[]> mEndActionsForProperty = + new HashMap<>(); + + /** + * Start velocities to use for TRANSLATION_X and TRANSLATION_Y, since these are often + * provided by VelocityTrackers and differ from each other. + */ + private Map<DynamicAnimation.ViewProperty, Float> mPositionStartVelocities = + new HashMap<>(); + + /** + * End actions to call when both TRANSLATION_X and TRANSLATION_Y animations have completed, + * if {@link #position} was used to animate TRANSLATION_X and TRANSLATION_Y simultaneously. + */ + @Nullable private Runnable[] mPositionEndActions; + + /** + * All of the properties that have been set and will animate when {@link #start} is called. + */ + private Map<DynamicAnimation.ViewProperty, Float> mAnimatedProperties = new HashMap<>(); + + /** + * All of the initial property values that have been set. These values will be instantly set + * when {@link #start} is called, just before the animation begins. + */ + private Map<DynamicAnimation.ViewProperty, Float> mInitialPropertyValues = new HashMap<>(); + + /** The animation controller that last retrieved this animator instance. */ + private PhysicsAnimationController mAssociatedController; + + /** + * Animator used to traverse the path provided to {@link #followAnimatedTargetAlongPath}. As + * the path is traversed, the view's translation spring animation final positions are + * updated such that the view 'follows' the current position on the path. + */ + @Nullable private ObjectAnimator mPathAnimator; + + /** Current position on the path. This is animated by {@link #mPathAnimator}. */ + private PointF mCurrentPointOnPath = new PointF(); + + /** + * FloatProperty instances that can be passed to {@link ObjectAnimator} to animate the value + * of {@link #mCurrentPointOnPath}. + */ + private final FloatProperty<PhysicsPropertyAnimator> mCurrentPointOnPathXProperty = + new FloatProperty<PhysicsPropertyAnimator>("PathX") { + @Override + public void setValue(PhysicsPropertyAnimator object, float value) { + mCurrentPointOnPath.x = value; + } + + @Override + public Float get(PhysicsPropertyAnimator object) { + return mCurrentPointOnPath.x; + } + }; + + private final FloatProperty<PhysicsPropertyAnimator> mCurrentPointOnPathYProperty = + new FloatProperty<PhysicsPropertyAnimator>("PathY") { + @Override + public void setValue(PhysicsPropertyAnimator object, float value) { + mCurrentPointOnPath.y = value; + } + + @Override + public Float get(PhysicsPropertyAnimator object) { + return mCurrentPointOnPath.y; + } + }; + + protected PhysicsPropertyAnimator(View view) { + this.mView = view; + } + + /** Animate a property to the given value, then call the optional end actions. */ + public PhysicsPropertyAnimator property( + DynamicAnimation.ViewProperty property, float value, Runnable... endActions) { + mAnimatedProperties.put(property, value); + mEndActionsForProperty.put(property, endActions); + return this; + } + + /** Animate the view's alpha value to the provided value. */ + public PhysicsPropertyAnimator alpha(float alpha, Runnable... endActions) { + return property(DynamicAnimation.ALPHA, alpha, endActions); + } + + /** Set the view's alpha value to 'from', then animate it to the given value. */ + public PhysicsPropertyAnimator alpha(float from, float to, Runnable... endActions) { + mInitialPropertyValues.put(DynamicAnimation.ALPHA, from); + return alpha(to, endActions); + } + + /** Animate the view's translationX value to the provided value. */ + public PhysicsPropertyAnimator translationX(float translationX, Runnable... endActions) { + mPathAnimator = null; // We aren't using the path anymore if we're translating. + return property(DynamicAnimation.TRANSLATION_X, translationX, endActions); + } + + /** Set the view's translationX value to 'from', then animate it to the given value. */ + public PhysicsPropertyAnimator translationX( + float from, float to, Runnable... endActions) { + mInitialPropertyValues.put(DynamicAnimation.TRANSLATION_X, from); + return translationX(to, endActions); + } + + /** Animate the view's translationY value to the provided value. */ + public PhysicsPropertyAnimator translationY(float translationY, Runnable... endActions) { + mPathAnimator = null; // We aren't using the path anymore if we're translating. + return property(DynamicAnimation.TRANSLATION_Y, translationY, endActions); + } + + /** Set the view's translationY value to 'from', then animate it to the given value. */ + public PhysicsPropertyAnimator translationY( + float from, float to, Runnable... endActions) { + mInitialPropertyValues.put(DynamicAnimation.TRANSLATION_Y, from); + return translationY(to, endActions); + } + + /** + * Animate the view's translationX and translationY values, and call the end actions only + * once both TRANSLATION_X and TRANSLATION_Y animations have completed. + */ + public PhysicsPropertyAnimator position( + float translationX, float translationY, Runnable... endActions) { + mPositionEndActions = endActions; + translationX(translationX); + return translationY(translationY); + } + + /** + * Animates a 'target' point that moves along the given path, using the provided duration + * and interpolator to animate the target. The view itself is animated using physics-based + * animations, whose final positions are updated to the target position as it animates. This + * results in the view 'following' the target in a realistic way. + * + * This method will override earlier calls to {@link #translationX}, {@link #translationY}, + * or {@link #position}, ultimately animating the view's position to the final point on the + * given path. + * + * @param pathAnimEndActions End actions to run after the animator that moves the target + * along the path ends. The views following the target may still + * be moving. + */ + public PhysicsPropertyAnimator followAnimatedTargetAlongPath( + Path path, + int targetAnimDuration, + TimeInterpolator targetAnimInterpolator, + Runnable... pathAnimEndActions) { + if (mPathAnimator != null) { + mPathAnimator.cancel(); + } + + mPathAnimator = ObjectAnimator.ofFloat( + this, mCurrentPointOnPathXProperty, mCurrentPointOnPathYProperty, path); + + if (pathAnimEndActions != null) { + mPathAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + for (Runnable action : pathAnimEndActions) { + if (action != null) { + action.run(); + } + } + } + }); + } + + mPathAnimator.setDuration(targetAnimDuration); + mPathAnimator.setInterpolator(targetAnimInterpolator); + + // Remove translation related values since we're going to ignore them and follow the + // path instead. + clearTranslationValues(); + return this; + } + + private void clearTranslationValues() { + mAnimatedProperties.remove(DynamicAnimation.TRANSLATION_X); + mAnimatedProperties.remove(DynamicAnimation.TRANSLATION_Y); + mInitialPropertyValues.remove(DynamicAnimation.TRANSLATION_X); + mInitialPropertyValues.remove(DynamicAnimation.TRANSLATION_Y); + mEndActionForProperty.remove(DynamicAnimation.TRANSLATION_X); + mEndActionForProperty.remove(DynamicAnimation.TRANSLATION_Y); + } + + /** Animate the view's scaleX value to the provided value. */ + public PhysicsPropertyAnimator scaleX(float scaleX, Runnable... endActions) { + return property(DynamicAnimation.SCALE_X, scaleX, endActions); + } + + /** Set the view's scaleX value to 'from', then animate it to the given value. */ + public PhysicsPropertyAnimator scaleX(float from, float to, Runnable... endActions) { + mInitialPropertyValues.put(DynamicAnimation.SCALE_X, from); + return scaleX(to, endActions); + } + + /** Animate the view's scaleY value to the provided value. */ + public PhysicsPropertyAnimator scaleY(float scaleY, Runnable... endActions) { + return property(DynamicAnimation.SCALE_Y, scaleY, endActions); + } + + /** Set the view's scaleY value to 'from', then animate it to the given value. */ + public PhysicsPropertyAnimator scaleY(float from, float to, Runnable... endActions) { + mInitialPropertyValues.put(DynamicAnimation.SCALE_Y, from); + return scaleY(to, endActions); + } + + /** Set the start velocity to use for all property animations. */ + public PhysicsPropertyAnimator withStartVelocity(float startVel) { + mDefaultStartVelocity = startVel; + return this; + } + + /** + * Set the damping ratio to use for this animation. If not supplied, will default to the + * value from {@link PhysicsAnimationController#getSpringForce}. + */ + public PhysicsPropertyAnimator withDampingRatio(float dampingRatio) { + mDampingRatio = dampingRatio; + return this; + } + + /** + * Set the stiffness to use for this animation. If not supplied, will default to the + * value from {@link PhysicsAnimationController#getSpringForce}. + */ + public PhysicsPropertyAnimator withStiffness(float stiffness) { + mStiffness = stiffness; + return this; + } + + /** + * Set the start velocities to use for TRANSLATION_X and TRANSLATION_Y animations. This + * overrides any value set via {@link #withStartVelocity(float)} for those properties. + */ + public PhysicsPropertyAnimator withPositionStartVelocities(float velX, float velY) { + mPositionStartVelocities.put(DynamicAnimation.TRANSLATION_X, velX); + mPositionStartVelocities.put(DynamicAnimation.TRANSLATION_Y, velY); + return this; + } + + /** Set a delay, in milliseconds, before kicking off the animations. */ + public PhysicsPropertyAnimator withStartDelay(long startDelay) { + mStartDelay = startDelay; + return this; + } + + /** + * Start the animations, and call the optional end actions once all animations for every + * animated property on every child (including chained animations) have ended. + */ + public void start(Runnable... after) { + if (!isActiveController(mAssociatedController)) { + Log.w(TAG, "Only the active animation controller is allowed to start animations. " + + "Use PhysicsAnimationLayout#setActiveController to set the active " + + "animation controller."); + return; + } + + final Set<DynamicAnimation.ViewProperty> properties = getAnimatedProperties(); + + // If there are end actions, set an end listener on the layout for all the properties + // we're about to animate. + if (after != null && after.length > 0) { + final DynamicAnimation.ViewProperty[] propertiesArray = + properties.toArray(new DynamicAnimation.ViewProperty[0]); + mAssociatedController.setEndActionForMultipleProperties(() -> { + for (Runnable callback : after) { + callback.run(); + } + }, propertiesArray); + } + + // If we used position-specific end actions, we'll need to listen for both TRANSLATION_X + // and TRANSLATION_Y animations ending, and call them once both have finished. + if (mPositionEndActions != null) { + final SpringAnimation translationXAnim = + getSpringAnimationFromView(DynamicAnimation.TRANSLATION_X, mView); + final SpringAnimation translationYAnim = + getSpringAnimationFromView(DynamicAnimation.TRANSLATION_Y, mView); + final Runnable waitForBothXAndY = () -> { + if (!translationXAnim.isRunning() && !translationYAnim.isRunning()) { + if (mPositionEndActions != null) { + for (Runnable callback : mPositionEndActions) { + callback.run(); + } + } + + mPositionEndActions = null; + } + }; + + mEndActionsForProperty.put(DynamicAnimation.TRANSLATION_X, + new Runnable[]{waitForBothXAndY}); + mEndActionsForProperty.put(DynamicAnimation.TRANSLATION_Y, + new Runnable[]{waitForBothXAndY}); + } + + if (mPathAnimator != null) { + startPathAnimation(); + } + + // Actually start the animations. + for (DynamicAnimation.ViewProperty property : properties) { + // Don't start translation animations if we're using a path animator, the update + // listeners added to that animator will take care of that. + if (mPathAnimator != null + && (property.equals(DynamicAnimation.TRANSLATION_X) + || property.equals(DynamicAnimation.TRANSLATION_Y))) { + return; + } + + if (mInitialPropertyValues.containsKey(property)) { + property.setValue(mView, mInitialPropertyValues.get(property)); + } + + final SpringForce defaultSpringForce = mController.getSpringForce(property, mView); + animateValueForChild( + property, + mView, + mAnimatedProperties.get(property), + mPositionStartVelocities.getOrDefault(property, mDefaultStartVelocity), + mStartDelay, + mStiffness >= 0 ? mStiffness : defaultSpringForce.getStiffness(), + mDampingRatio >= 0 ? mDampingRatio : defaultSpringForce.getDampingRatio(), + mEndActionsForProperty.get(property)); + } + + clearAnimator(); + } + + /** Returns the set of properties that will animate once {@link #start} is called. */ + protected Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { + final HashSet<DynamicAnimation.ViewProperty> animatedProperties = new HashSet<>( + mAnimatedProperties.keySet()); + + // If we're using a path animator, it'll kick off translation animations. + if (mPathAnimator != null) { + animatedProperties.add(DynamicAnimation.TRANSLATION_X); + animatedProperties.add(DynamicAnimation.TRANSLATION_Y); + } + + return animatedProperties; + } + + /** + * Animates the property of the given child view, then runs the callback provided when the + * animation ends. + */ + protected void animateValueForChild( + DynamicAnimation.ViewProperty property, + View view, + float value, + float startVel, + long startDelay, + float stiffness, + float dampingRatio, + Runnable... afterCallbacks) { + if (view != null) { + final SpringAnimation animation = + (SpringAnimation) view.getTag(getTagIdForProperty(property)); + + // If the animation is null, the view was probably removed from the layout before + // the animation started. + if (animation == null) { + return; + } + + if (afterCallbacks != null) { + animation.addEndListener(new OneTimeEndListener() { + @Override + public void onAnimationEnd(DynamicAnimation animation, boolean canceled, + float value, float velocity) { + super.onAnimationEnd(animation, canceled, value, velocity); + for (Runnable runnable : afterCallbacks) { + runnable.run(); + } + } + }); + } + + final SpringForce animationSpring = animation.getSpring(); + + if (animationSpring == null) { + return; + } + + final Runnable configureAndStartAnimation = () -> { + animationSpring.setStiffness(stiffness); + animationSpring.setDampingRatio(dampingRatio); + + if (startVel > -Float.MAX_VALUE) { + animation.setStartVelocity(startVel); + } + + animationSpring.setFinalPosition(value); + animation.start(); + }; + + if (startDelay > 0) { + postDelayed(configureAndStartAnimation, startDelay); + } else { + configureAndStartAnimation.run(); + } + } + } + + /** + * Updates the final position of a view's animation, without changing any of the animation's + * other settings. Calling this before an initial call to {@link #animateValueForChild} will + * work, but result in unknown values for stiffness, etc. and is not recommended. + */ + private void updateValueForChild( + DynamicAnimation.ViewProperty property, View view, float position) { + if (view != null) { + final SpringAnimation animation = + (SpringAnimation) view.getTag(getTagIdForProperty(property)); + + if (animation == null) { + return; + } + + final SpringForce animationSpring = animation.getSpring(); + + if (animationSpring == null) { + return; + } + + animationSpring.setFinalPosition(position); + animation.start(); + } + } + + /** + * Configures the path animator to respect the settings passed into the animation builder + * and adds update listeners that update the translation physics animations. Then, starts + * the path animation. + */ + protected void startPathAnimation() { + final SpringForce defaultSpringForceX = mController.getSpringForce( + DynamicAnimation.TRANSLATION_X, mView); + final SpringForce defaultSpringForceY = mController.getSpringForce( + DynamicAnimation.TRANSLATION_Y, mView); + + if (mStartDelay > 0) { + mPathAnimator.setStartDelay(mStartDelay); + } + + final Runnable updatePhysicsAnims = () -> { + updateValueForChild( + DynamicAnimation.TRANSLATION_X, mView, mCurrentPointOnPath.x); + updateValueForChild( + DynamicAnimation.TRANSLATION_Y, mView, mCurrentPointOnPath.y); + }; + + mPathAnimator.addUpdateListener(pathAnim -> updatePhysicsAnims.run()); + mPathAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + animateValueForChild( + DynamicAnimation.TRANSLATION_X, + mView, + mCurrentPointOnPath.x, + mDefaultStartVelocity, + 0 /* startDelay */, + mStiffness >= 0 ? mStiffness : defaultSpringForceX.getStiffness(), + mDampingRatio >= 0 + ? mDampingRatio + : defaultSpringForceX.getDampingRatio()); + + animateValueForChild( + DynamicAnimation.TRANSLATION_Y, + mView, + mCurrentPointOnPath.y, + mDefaultStartVelocity, + 0 /* startDelay */, + mStiffness >= 0 ? mStiffness : defaultSpringForceY.getStiffness(), + mDampingRatio >= 0 + ? mDampingRatio + : defaultSpringForceY.getDampingRatio()); + } + + @Override + public void onAnimationEnd(Animator animation) { + updatePhysicsAnims.run(); + } + }); + + // If there's a target animator saved for the view, make sure it's not running. + final ObjectAnimator targetAnimator = getTargetAnimatorFromView(mView); + if (targetAnimator != null) { + targetAnimator.cancel(); + } + + mView.setTag(R.id.target_animator_tag, mPathAnimator); + mPathAnimator.start(); + } + + private void clearAnimator() { + mInitialPropertyValues.clear(); + mAnimatedProperties.clear(); + mPositionStartVelocities.clear(); + mDefaultStartVelocity = -Float.MAX_VALUE; + mStartDelay = 0; + mStiffness = -1; + mDampingRatio = -1; + mEndActionsForProperty.clear(); + mPathAnimator = null; + mPositionEndActions = null; + } + + /** + * Sets the controller that last retrieved this animator instance, so that we can prevent + * {@link #start} from actually starting animations if called by a non-active controller. + */ + private void setAssociatedController(PhysicsAnimationController controller) { + mAssociatedController = controller; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java new file mode 100644 index 000000000000..73371e7eff20 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java @@ -0,0 +1,1083 @@ +/* + * 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.bubbles.animation; + +import android.content.ContentResolver; +import android.content.res.Resources; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.provider.Settings; +import android.util.Log; +import android.view.View; +import android.view.ViewPropertyAnimator; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +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.R; +import com.android.wm.shell.animation.PhysicsAnimator; +import com.android.wm.shell.bubbles.BadgedImageView; +import com.android.wm.shell.bubbles.BubblePositioner; +import com.android.wm.shell.bubbles.BubbleStackView; +import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; + +import com.google.android.collect.Sets; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.List; +import java.util.Set; +import java.util.function.IntSupplier; + +/** + * Animation controller for bubbles when they're in their stacked state. Stacked bubbles sit atop + * each other with a slight offset to the left or right (depending on which side of the screen they + * are on). Bubbles 'follow' each other when dragged, and can be flung to the left or right sides of + * the screen. + */ +public class StackAnimationController extends + PhysicsAnimationLayout.PhysicsAnimationController { + + private static final String TAG = "Bubbs.StackCtrl"; + + /** Value to use for animating bubbles in and springing stack after fling. */ + private static final float STACK_SPRING_STIFFNESS = 700f; + + /** Values to use for animating updated bubble to top of stack. */ + private static final float NEW_BUBBLE_START_SCALE = 0.5f; + private static final float NEW_BUBBLE_START_Y = 100f; + private static final long BUBBLE_SWAP_DURATION = 300L; + + /** + * Values to use for the default {@link SpringForce} provided to the physics animation layout. + */ + public static final int SPRING_TO_TOUCH_STIFFNESS = 12000; + public static final float IME_ANIMATION_STIFFNESS = SpringForce.STIFFNESS_LOW; + private static final int CHAIN_STIFFNESS = 600; + public static final float DEFAULT_BOUNCINESS = 0.9f; + + private final PhysicsAnimator.SpringConfig mAnimateOutSpringConfig = + new PhysicsAnimator.SpringConfig( + STACK_SPRING_STIFFNESS, SpringForce.DAMPING_RATIO_NO_BOUNCY); + + /** + * Friction applied to fling animations. Since the stack must land on one of the sides of the + * screen, we want less friction horizontally so that the stack has a better chance of making it + * to the side without needing a spring. + */ + private static final float FLING_FRICTION = 1.9f; + + private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f; + + /** Sentinel value for unset position value. */ + private static final float UNSET = -Float.MIN_VALUE; + + /** + * Minimum fling velocity required to trigger moving the stack from one side of the screen to + * the other. + */ + private static final float ESCAPE_VELOCITY = 750f; + + /** Velocity required to dismiss the stack without dragging it into the dismiss target. */ + private static final float FLING_TO_DISMISS_MIN_VELOCITY = 4000f; + + /** + * The canonical position of the stack. This is typically the position of the first bubble, but + * we need to keep track of it separately from the first bubble's translation in case there are + * no bubbles, or the first bubble was just added and being animated to its new position. + */ + private PointF mStackPosition = new PointF(-1, -1); + + /** + * MagnetizedObject instance for the stack, which is used by the touch handler for the magnetic + * dismiss target. + */ + private MagnetizedObject<StackAnimationController> mMagnetizedStack; + + /** + * The area that Bubbles will occupy after all animations end. This is used to move other + * floating content out of the way proactively. + */ + private Rect mAnimatingToBounds = new Rect(); + + /** Initial starting location for the stack. */ + @Nullable private BubbleStackView.RelativeStackPosition mStackStartPosition; + + /** Whether or not the stack's start position has been set. */ + private boolean mStackMovedToStartPosition = false; + + /** The height of the most recently visible IME. */ + private float mImeHeight = 0f; + + /** + * The Y position of the stack before the IME became visible, or {@link Float#MIN_VALUE} if the + * IME is not visible or the user moved the stack since the IME became visible. + */ + private float mPreImeY = UNSET; + + /** + * Animations on the stack position itself, which would have been started in + * {@link #flingThenSpringFirstBubbleWithStackFollowing}. These animations dispatch to + * {@link #moveFirstBubbleWithStackFollowing} to move the entire stack (with 'following' effect) + * to a legal position on the side of the screen. + */ + private HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mStackPositionAnimations = + new HashMap<>(); + + /** + * Whether the current motion of the stack is due to a fling animation (vs. being dragged + * manually). + */ + private boolean mIsMovingFromFlinging = false; + + /** + * Whether the first bubble is springing towards the touch point, rather than using the default + * behavior of moving directly to the touch point with the rest of the stack following it. + * + * This happens when the user's finger exits the dismiss area while the stack is magnetized to + * the center. Since the touch point differs from the stack location, we need to animate the + * stack back to the touch point to avoid a jarring instant location change from the center of + * the target to the touch point just outside the target bounds. + * + * This is reset once the spring animations end, since that means the first bubble has + * successfully 'caught up' to the touch. + */ + private boolean mFirstBubbleSpringingToTouch = false; + + /** + * Whether to spring the stack to the next touch event coordinates. This is used to animate the + * stack (including the first bubble) out of the magnetic dismiss target to the touch location. + * Once it 'catches up' and the animation ends, we'll revert to moving the first bubble directly + * and only animating the following bubbles. + */ + private boolean mSpringToTouchOnNextMotionEvent = false; + + /** Horizontal offset of bubbles in the stack. */ + private float mStackOffset; + /** Offset between stack y and animation y for bubble swap. */ + private float mSwapAnimationOffset; + /** Max number of bubbles to show in the expanded bubble row. */ + private int mMaxBubbles; + /** Default bubble elevation. */ + private int mElevation; + /** Diameter of the bubble icon. */ + private int mBubbleBitmapSize; + /** Width of the bubble (icon and padding). */ + private int mBubbleSize; + /** + * The amount of space to add between the bubbles and certain UI elements, such as the top of + * the screen or the IME. This does not apply to the left/right sides of the screen since the + * stack goes offscreen intentionally. + */ + private int mBubblePaddingTop; + /** How far offscreen the stack rests. */ + private int mBubbleOffscreen; + /** Contains display size, orientation, and inset information. */ + private BubblePositioner mPositioner; + + /** FloatingContentCoordinator instance for resolving floating content conflicts. */ + private FloatingContentCoordinator mFloatingContentCoordinator; + + /** + * FloatingContent instance that returns the stack's location on the screen, and moves it when + * requested. + */ + private final FloatingContentCoordinator.FloatingContent mStackFloatingContent = + new FloatingContentCoordinator.FloatingContent() { + + private final Rect mFloatingBoundsOnScreen = new Rect(); + + @Override + public void moveToBounds(@NonNull Rect bounds) { + springStack(bounds.left, bounds.top, STACK_SPRING_STIFFNESS); + } + + @NonNull + @Override + public Rect getAllowedFloatingBoundsRegion() { + final Rect floatingBounds = getFloatingBoundsOnScreen(); + final Rect allowableStackArea = new Rect(); + getAllowableStackPositionRegion().roundOut(allowableStackArea); + allowableStackArea.right += floatingBounds.width(); + allowableStackArea.bottom += floatingBounds.height(); + return allowableStackArea; + } + + @NonNull + @Override + public Rect getFloatingBoundsOnScreen() { + if (!mAnimatingToBounds.isEmpty()) { + return mAnimatingToBounds; + } + + if (mLayout.getChildCount() > 0) { + // Calculate the bounds using stack position + bubble size so that we don't need to + // wait for the bubble views to lay out. + mFloatingBoundsOnScreen.set( + (int) mStackPosition.x, + (int) mStackPosition.y, + (int) mStackPosition.x + mBubbleSize, + (int) mStackPosition.y + mBubbleSize + mBubblePaddingTop); + } else { + mFloatingBoundsOnScreen.setEmpty(); + } + + return mFloatingBoundsOnScreen; + } + }; + + /** Returns the number of 'real' bubbles (excluding the overflow bubble). */ + private IntSupplier mBubbleCountSupplier; + + /** + * Callback to run whenever any bubble is animated out. The BubbleStackView will check if the + * end of this animation means we have no bubbles left, and notify the BubbleController. + */ + private Runnable mOnBubbleAnimatedOutAction; + + public StackAnimationController( + FloatingContentCoordinator floatingContentCoordinator, + IntSupplier bubbleCountSupplier, + Runnable onBubbleAnimatedOutAction, + BubblePositioner positioner) { + mFloatingContentCoordinator = floatingContentCoordinator; + mBubbleCountSupplier = bubbleCountSupplier; + mOnBubbleAnimatedOutAction = onBubbleAnimatedOutAction; + mPositioner = positioner; + } + + /** + * Instantly move the first bubble to the given point, and animate the rest of the stack behind + * it with the 'following' effect. + */ + public void moveFirstBubbleWithStackFollowing(float x, float y) { + // If we're moving the bubble around, we're not animating to any bounds. + mAnimatingToBounds.setEmpty(); + + // If we manually move the bubbles with the IME open, clear the return point since we don't + // want the stack to snap away from the new position. + mPreImeY = UNSET; + + moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X, x); + moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y, y); + + // This method is called when the stack is being dragged manually, so we're clearly no + // longer flinging. + mIsMovingFromFlinging = false; + } + + /** + * The position of the stack - typically the position of the first bubble; if no bubbles have + * been added yet, it will be where the first bubble will go when added. + */ + public PointF getStackPosition() { + return mStackPosition; + } + + /** Whether the stack is on the left side of the screen. */ + public boolean isStackOnLeftSide() { + if (mLayout == null || !isStackPositionSet()) { + return true; // Default to left, which is where it starts by default. + } + + float stackCenter = mStackPosition.x + mBubbleBitmapSize / 2; + float screenCenter = mLayout.getWidth() / 2; + return stackCenter < screenCenter; + } + + /** + * Fling stack to given corner, within allowable screen bounds. + * Note that we need new SpringForce instances per animation despite identical configs because + * SpringAnimation uses SpringForce's internal (changing) velocity while the animation runs. + */ + public void springStack( + float destinationX, float destinationY, float stiffness) { + notifyFloatingCoordinatorStackAnimatingTo(destinationX, destinationY); + + springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X, + new SpringForce() + .setStiffness(stiffness) + .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO), + 0 /* startXVelocity */, + destinationX); + + springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y, + new SpringForce() + .setStiffness(stiffness) + .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO), + 0 /* startYVelocity */, + destinationY); + } + + /** + * Springs the stack to the specified x/y coordinates, with the stiffness used for springs after + * flings. + */ + public void springStackAfterFling(float destinationX, float destinationY) { + springStack(destinationX, destinationY, STACK_SPRING_STIFFNESS); + } + + /** + * Flings the stack starting with the given velocities, springing it to the nearest edge + * afterward. + * + * @return The X value that the stack will end up at after the fling/spring. + */ + public float flingStackThenSpringToEdge(float x, float velX, float velY) { + final boolean stackOnLeftSide = x - mBubbleBitmapSize / 2 < mLayout.getWidth() / 2; + + final boolean stackShouldFlingLeft = stackOnLeftSide + ? velX < ESCAPE_VELOCITY + : velX < -ESCAPE_VELOCITY; + + final RectF stackBounds = getAllowableStackPositionRegion(); + + // Target X translation (either the left or right side of the screen). + final float destinationRelativeX = stackShouldFlingLeft + ? stackBounds.left : stackBounds.right; + + // If all bubbles were removed during a drag event, just return the X we would have animated + // to if there were still bubbles. + if (mLayout == null || mLayout.getChildCount() == 0) { + return destinationRelativeX; + } + + final ContentResolver contentResolver = mLayout.getContext().getContentResolver(); + final float stiffness = Settings.Secure.getFloat(contentResolver, "bubble_stiffness", + STACK_SPRING_STIFFNESS /* default */); + final float dampingRatio = Settings.Secure.getFloat(contentResolver, "bubble_damping", + SPRING_AFTER_FLING_DAMPING_RATIO); + final float friction = Settings.Secure.getFloat(contentResolver, "bubble_friction", + FLING_FRICTION); + + // Minimum velocity required for the stack to make it to the targeted side of the screen, + // taking friction into account (4.2f is the number that friction scalars are multiplied by + // in DynamicAnimation.DragForce). This is an estimate - it could possibly be slightly off, + // but the SpringAnimation at the end will ensure that it reaches the destination X + // regardless. + final float minimumVelocityToReachEdge = + (destinationRelativeX - x) * (friction * 4.2f); + + final float estimatedY = PhysicsAnimator.estimateFlingEndValue( + mStackPosition.y, velY, + new PhysicsAnimator.FlingConfig( + friction, stackBounds.top, stackBounds.bottom)); + + notifyFloatingCoordinatorStackAnimatingTo(destinationRelativeX, estimatedY); + + // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity so + // that it'll make it all the way to the side of the screen. + final float startXVelocity = stackShouldFlingLeft + ? Math.min(minimumVelocityToReachEdge, velX) + : Math.max(minimumVelocityToReachEdge, velX); + + + + flingThenSpringFirstBubbleWithStackFollowing( + DynamicAnimation.TRANSLATION_X, + startXVelocity, + friction, + new SpringForce() + .setStiffness(stiffness) + .setDampingRatio(dampingRatio), + destinationRelativeX); + + flingThenSpringFirstBubbleWithStackFollowing( + DynamicAnimation.TRANSLATION_Y, + velY, + friction, + new SpringForce() + .setStiffness(stiffness) + .setDampingRatio(dampingRatio), + /* destination */ null); + + // If we're flinging now, there's no more touch event to catch up to. + mFirstBubbleSpringingToTouch = false; + mIsMovingFromFlinging = true; + return destinationRelativeX; + } + + /** + * Where the stack would be if it were snapped to the nearest horizontal edge (left or right). + */ + public PointF getStackPositionAlongNearestHorizontalEdge() { + if (mPositioner.showingInTaskbar()) { + return mPositioner.getRestingPosition(); + } + final PointF stackPos = getStackPosition(); + final boolean onLeft = mLayout.isFirstChildXLeftOfCenter(stackPos.x); + final RectF bounds = getAllowableStackPositionRegion(); + + stackPos.x = onLeft ? bounds.left : bounds.right; + return stackPos; + } + + /** Description of current animation controller state. */ + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("StackAnimationController state:"); + pw.print(" isActive: "); pw.println(isActiveController()); + pw.print(" restingStackPos: "); + pw.println(mPositioner.getRestingPosition().toString()); + pw.print(" currentStackPos: "); pw.println(mStackPosition.toString()); + pw.print(" isMovingFromFlinging: "); pw.println(mIsMovingFromFlinging); + pw.print(" withinDismiss: "); pw.println(isStackStuckToTarget()); + pw.print(" firstBubbleSpringing: "); pw.println(mFirstBubbleSpringingToTouch); + } + + /** + * Flings the first bubble along the given property's axis, using the provided configuration + * values. When the animation ends - either by hitting the min/max, or by friction sufficiently + * reducing momentum - a SpringAnimation takes over to snap the bubble to the given final + * position. + */ + protected void flingThenSpringFirstBubbleWithStackFollowing( + DynamicAnimation.ViewProperty property, + float vel, + float friction, + SpringForce spring, + Float finalPosition) { + if (!isActiveController()) { + return; + } + + Log.d(TAG, String.format("Flinging %s.", + PhysicsAnimationLayout.getReadablePropertyName(property))); + + StackPositionProperty firstBubbleProperty = new StackPositionProperty(property); + final float currentValue = firstBubbleProperty.getValue(this); + final RectF bounds = getAllowableStackPositionRegion(); + final float min = + property.equals(DynamicAnimation.TRANSLATION_X) + ? bounds.left + : bounds.top; + final float max = + property.equals(DynamicAnimation.TRANSLATION_X) + ? bounds.right + : bounds.bottom; + + FlingAnimation flingAnimation = new FlingAnimation(this, firstBubbleProperty); + flingAnimation.setFriction(friction) + .setStartVelocity(vel) + + // If the bubble's property value starts beyond the desired min/max, use that value + // instead so that the animation won't immediately end. If, for example, the user + // drags the bubbles into the navigation bar, but then flings them upward, we want + // the fling to occur despite temporarily having a value outside of the min/max. If + // the bubbles are out of bounds and flung even farther out of bounds, the fling + // animation will halt immediately and the SpringAnimation will take over, springing + // it in reverse to the (legal) final position. + .setMinValue(Math.min(currentValue, min)) + .setMaxValue(Math.max(currentValue, max)) + + .addEndListener((animation, canceled, endValue, endVelocity) -> { + if (!canceled) { + mPositioner.setRestingPosition(mStackPosition); + + springFirstBubbleWithStackFollowing(property, spring, endVelocity, + finalPosition != null + ? finalPosition + : Math.max(min, Math.min(max, endValue))); + } + }); + + cancelStackPositionAnimation(property); + mStackPositionAnimations.put(property, flingAnimation); + flingAnimation.start(); + } + + /** + * Cancel any stack position animations that were started by calling + * @link #flingThenSpringFirstBubbleWithStackFollowing}, and remove any corresponding end + * listeners. + */ + public void cancelStackPositionAnimations() { + cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_X); + cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_Y); + + removeEndActionForProperty(DynamicAnimation.TRANSLATION_X); + removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y); + } + + /** Save the current IME height so that we know where the stack bounds should be. */ + public void setImeHeight(int imeHeight) { + mImeHeight = imeHeight; + } + + /** + * Animates the stack either away from the newly visible IME, or back to its original position + * due to the IME going away. + * + * @return The destination Y value of the stack due to the IME movement (or the current position + * of the stack if it's not moving). + */ + public float animateForImeVisibility(boolean imeVisible) { + final float maxBubbleY = getAllowableStackPositionRegion().bottom; + float destinationY = UNSET; + + if (imeVisible) { + // Stack is lower than it should be and overlaps the now-visible IME. + if (mStackPosition.y > maxBubbleY && mPreImeY == UNSET) { + mPreImeY = mStackPosition.y; + destinationY = maxBubbleY; + } + } else { + if (mPreImeY != UNSET) { + destinationY = mPreImeY; + mPreImeY = UNSET; + } + } + + if (destinationY != UNSET) { + springFirstBubbleWithStackFollowing( + DynamicAnimation.TRANSLATION_Y, + getSpringForce(DynamicAnimation.TRANSLATION_Y, /* view */ null) + .setStiffness(IME_ANIMATION_STIFFNESS), + /* startVel */ 0f, + destinationY); + + notifyFloatingCoordinatorStackAnimatingTo(mStackPosition.x, destinationY); + } + + return destinationY != UNSET ? destinationY : mStackPosition.y; + } + + /** + * Notifies the floating coordinator that we're moving, and sets {@link #mAnimatingToBounds} so + * we return these bounds from + * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}. + */ + private void notifyFloatingCoordinatorStackAnimatingTo(float x, float y) { + final Rect floatingBounds = mStackFloatingContent.getFloatingBoundsOnScreen(); + floatingBounds.offsetTo((int) x, (int) y); + mAnimatingToBounds = floatingBounds; + mFloatingContentCoordinator.onContentMoved(mStackFloatingContent); + } + + /** + * Returns the region that the stack position must stay within. This goes slightly off the left + * and right sides of the screen, below the status bar/cutout and above the navigation bar. + * While the stack position is not allowed to rest outside of these bounds, it can temporarily + * be animated or dragged beyond them. + */ + public RectF getAllowableStackPositionRegion() { + final RectF allowableRegion = new RectF(mPositioner.getAvailableRect()); + allowableRegion.left -= mBubbleOffscreen; + allowableRegion.top += mBubblePaddingTop; + allowableRegion.right += mBubbleOffscreen - mBubbleSize; + allowableRegion.bottom -= mBubblePaddingTop + mBubbleSize + + (mImeHeight != UNSET ? mImeHeight + mBubblePaddingTop : 0f); + return allowableRegion; + } + + /** Moves the stack in response to a touch event. */ + public void moveStackFromTouch(float x, float y) { + // Begin the spring-to-touch catch up animation if needed. + if (mSpringToTouchOnNextMotionEvent) { + springStack(x, y, SPRING_TO_TOUCH_STIFFNESS); + mSpringToTouchOnNextMotionEvent = false; + mFirstBubbleSpringingToTouch = true; + } else if (mFirstBubbleSpringingToTouch) { + final SpringAnimation springToTouchX = + (SpringAnimation) mStackPositionAnimations.get( + DynamicAnimation.TRANSLATION_X); + final SpringAnimation springToTouchY = + (SpringAnimation) mStackPositionAnimations.get( + DynamicAnimation.TRANSLATION_Y); + + // If either animation is still running, we haven't caught up. Update the animations. + if (springToTouchX.isRunning() || springToTouchY.isRunning()) { + springToTouchX.animateToFinalPosition(x); + springToTouchY.animateToFinalPosition(y); + } else { + // If the animations have finished, the stack is now at the touch point. We can + // resume moving the bubble directly. + mFirstBubbleSpringingToTouch = false; + } + } + + if (!mFirstBubbleSpringingToTouch && !isStackStuckToTarget()) { + moveFirstBubbleWithStackFollowing(x, y); + } + } + + /** Notify the controller that the stack has been unstuck from the dismiss target. */ + public void onUnstuckFromTarget() { + mSpringToTouchOnNextMotionEvent = true; + } + + /** + * 'Implode' the stack by shrinking the bubbles, fading them out, and translating them down. + */ + public void animateStackDismissal(float translationYBy, Runnable after) { + animationsForChildrenFromIndex(0, (index, animation) -> + animation + .scaleX(0f) + .scaleY(0f) + .alpha(0f) + .translationY( + mLayout.getChildAt(index).getTranslationY() + translationYBy) + .withStiffness(SpringForce.STIFFNESS_HIGH)) + .startAll(after); + } + + /** + * Springs the first bubble to the given final position, with the rest of the stack 'following'. + */ + protected void springFirstBubbleWithStackFollowing( + DynamicAnimation.ViewProperty property, SpringForce spring, + float vel, float finalPosition, @Nullable Runnable... after) { + + if (mLayout.getChildCount() == 0 || !isActiveController()) { + return; + } + + Log.d(TAG, String.format("Springing %s to final position %f.", + PhysicsAnimationLayout.getReadablePropertyName(property), + finalPosition)); + + // Whether we're springing towards the touch location, rather than to a position on the + // sides of the screen. + final boolean isSpringingTowardsTouch = mSpringToTouchOnNextMotionEvent; + + StackPositionProperty firstBubbleProperty = new StackPositionProperty(property); + SpringAnimation springAnimation = + new SpringAnimation(this, firstBubbleProperty) + .setSpring(spring) + .addEndListener((dynamicAnimation, b, v, v1) -> { + if (!isSpringingTowardsTouch) { + // If we're springing towards the touch position, don't save the + // resting position - the touch location is not a valid resting + // position. We'll set this when the stack springs to the left or + // right side of the screen after the touch gesture ends. + mPositioner.setRestingPosition(mStackPosition); + } + + if (after != null) { + for (Runnable callback : after) { + callback.run(); + } + } + }) + .setStartVelocity(vel); + + cancelStackPositionAnimation(property); + mStackPositionAnimations.put(property, springAnimation); + springAnimation.animateToFinalPosition(finalPosition); + } + + @Override + Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { + return Sets.newHashSet( + DynamicAnimation.TRANSLATION_X, // For positioning. + DynamicAnimation.TRANSLATION_Y, + DynamicAnimation.ALPHA, // For fading in new bubbles. + DynamicAnimation.SCALE_X, // For 'popping in' new bubbles. + DynamicAnimation.SCALE_Y); + } + + @Override + int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) { + if (property.equals(DynamicAnimation.TRANSLATION_X) + || property.equals(DynamicAnimation.TRANSLATION_Y)) { + return index + 1; + } else { + return NONE; + } + } + + + @Override + float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) { + if (property.equals(DynamicAnimation.TRANSLATION_Y)) { + // If we're in the dismiss target, have the bubbles pile on top of each other with no + // offset. + if (isStackStuckToTarget()) { + return 0f; + } else { + return mStackOffset; + } + } else { + return 0f; + } + } + + @Override + SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) { + final ContentResolver contentResolver = mLayout.getContext().getContentResolver(); + final float dampingRatio = Settings.Secure.getFloat(contentResolver, "bubble_damping", + DEFAULT_BOUNCINESS); + + return new SpringForce() + .setDampingRatio(dampingRatio) + .setStiffness(CHAIN_STIFFNESS); + } + + @Override + void onChildAdded(View child, int index) { + // Don't animate additions within the dismiss target. + if (isStackStuckToTarget()) { + return; + } + + if (getBubbleCount() == 1) { + // If this is the first child added, position the stack in its starting position. + moveStackToStartPosition(); + } else if (isStackPositionSet() && mLayout.indexOfChild(child) == 0) { + // Otherwise, animate the bubble in if it's the newest bubble. If we're adding a bubble + // to the back of the stack, it'll be largely invisible so don't bother animating it in. + animateInBubble(child, index); + } + } + + @Override + void onChildRemoved(View child, int index, Runnable finishRemoval) { + PhysicsAnimator.getInstance(child) + .spring(DynamicAnimation.ALPHA, 0f) + .spring(DynamicAnimation.SCALE_X, 0f, mAnimateOutSpringConfig) + .spring(DynamicAnimation.SCALE_Y, 0f, mAnimateOutSpringConfig) + .withEndActions(finishRemoval, mOnBubbleAnimatedOutAction) + .start(); + + // If there are other bubbles, pull them into the correct position. + if (getBubbleCount() > 0) { + animationForChildAtIndex(0).translationX(mStackPosition.x).start(); + } else { + // When all children are removed ensure stack position is sane + mPositioner.setRestingPosition(mPositioner.getRestingPosition()); + + // Remove the stack from the coordinator since we don't have any bubbles and aren't + // visible. + mFloatingContentCoordinator.onContentRemoved(mStackFloatingContent); + } + } + + public void animateReorder(List<View> bubbleViews, Runnable after) { + // After the bubble going to index 0 springs above stack, update all icons + // at the same time, to avoid visibly changing bubble order before the animation completes. + Runnable updateAllIcons = () -> { + for (int newIndex = 0; newIndex < bubbleViews.size(); newIndex++) { + View view = bubbleViews.get(newIndex); + updateBadgesAndZOrder(view, newIndex); + } + }; + + for (int newIndex = 0; newIndex < bubbleViews.size(); newIndex++) { + View view = bubbleViews.get(newIndex); + final int oldIndex = mLayout.indexOfChild(view); + animateSwap(view, oldIndex, newIndex, updateAllIcons, after); + } + } + + private void animateSwap(View view, int oldIndex, int newIndex, + Runnable updateAllIcons, Runnable finishReorder) { + if (newIndex == oldIndex) { + // Add new bubble to index 0; move existing bubbles down + updateBadgesAndZOrder(view, newIndex); + if (newIndex == 0) { + animateInBubble(view, newIndex); + } else { + moveToFinalIndex(view, newIndex, finishReorder); + } + } else { + // Reorder existing bubbles + if (newIndex == 0) { + animateToFrontThenUpdateIcons(view, updateAllIcons, finishReorder); + } else { + moveToFinalIndex(view, newIndex, finishReorder); + } + } + } + + private void animateToFrontThenUpdateIcons(View v, Runnable updateAllIcons, + Runnable finishReorder) { + final ViewPropertyAnimator animator = v.animate() + .translationY(getStackPosition().y - mSwapAnimationOffset) + .setDuration(BUBBLE_SWAP_DURATION) + .withEndAction(() -> { + updateAllIcons.run(); + moveToFinalIndex(v, 0 /* index */, finishReorder); + }); + v.setTag(R.id.reorder_animator_tag, animator); + } + + private void moveToFinalIndex(View view, int newIndex, + Runnable finishReorder) { + final ViewPropertyAnimator animator = view.animate() + .translationY(getStackPosition().y + newIndex * mStackOffset) + .setDuration(BUBBLE_SWAP_DURATION) + .withEndAction(() -> { + view.setTag(R.id.reorder_animator_tag, null); + finishReorder.run(); + }); + view.setTag(R.id.reorder_animator_tag, animator); + } + + private void updateBadgesAndZOrder(View v, int index) { + v.setZ((mMaxBubbles * mElevation) - index); + BadgedImageView bv = (BadgedImageView) v; + if (index == 0) { + bv.showDotAndBadge(!isStackOnLeftSide()); + } else { + bv.hideDotAndBadge(!isStackOnLeftSide()); + } + } + + @Override + void onChildReordered(View child, int oldIndex, int newIndex) {} + + @Override + void onActiveControllerForLayout(PhysicsAnimationLayout layout) { + Resources res = layout.getResources(); + mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); + mSwapAnimationOffset = res.getDimensionPixelSize(R.dimen.bubble_swap_animation_offset); + mMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered); + mElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); + mBubbleSize = mPositioner.getBubbleSize(); + mBubbleBitmapSize = mPositioner.getBubbleBitmapSize(); + mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); + mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen); + } + + /** + * Update resources. + */ + public void updateResources() { + if (mLayout != null) { + Resources res = mLayout.getContext().getResources(); + mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); + } + } + + private boolean isStackStuckToTarget() { + return mMagnetizedStack != null && mMagnetizedStack.getObjectStuckToTarget(); + } + + /** Moves the stack, without any animation, to the starting position. */ + private void moveStackToStartPosition() { + // Post to ensure that the layout's width and height have been calculated. + mLayout.setVisibility(View.INVISIBLE); + mLayout.post(() -> { + setStackPosition(mPositioner.getRestingPosition()); + + mStackMovedToStartPosition = true; + mLayout.setVisibility(View.VISIBLE); + + // Animate in the top bubble now that we're visible. + if (mLayout.getChildCount() > 0) { + // Add the stack to the floating content coordinator now that we have a bubble and + // are visible. + mFloatingContentCoordinator.onContentAdded(mStackFloatingContent); + + animateInBubble(mLayout.getChildAt(0), 0 /* index */); + } + }); + } + + /** + * Moves the first bubble instantly to the given X or Y translation, and instructs subsequent + * bubbles to animate 'following' to the new location. + */ + private void moveFirstBubbleWithStackFollowing( + DynamicAnimation.ViewProperty property, float value) { + + // Update the canonical stack position. + if (property.equals(DynamicAnimation.TRANSLATION_X)) { + mStackPosition.x = value; + } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) { + mStackPosition.y = value; + } + + if (mLayout.getChildCount() > 0) { + property.setValue(mLayout.getChildAt(0), value); + if (mLayout.getChildCount() > 1) { + animationForChildAtIndex(1) + .property(property, value + getOffsetForChainedPropertyAnimation(property)) + .start(); + } + } + } + + /** Moves the stack to a position instantly, with no animation. */ + public void setStackPosition(PointF pos) { + Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y)); + mStackPosition.set(pos.x, pos.y); + + mPositioner.setRestingPosition(mStackPosition); + + // If we're not the active controller, we don't want to physically move the bubble views. + if (isActiveController()) { + // Cancel animations that could be moving the views. + mLayout.cancelAllAnimationsOfProperties( + DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + cancelStackPositionAnimations(); + + // Since we're not using the chained animations, apply the offsets manually. + final float xOffset = getOffsetForChainedPropertyAnimation( + DynamicAnimation.TRANSLATION_X); + final float yOffset = getOffsetForChainedPropertyAnimation( + DynamicAnimation.TRANSLATION_Y); + for (int i = 0; i < mLayout.getChildCount(); i++) { + mLayout.getChildAt(i).setTranslationX(pos.x + (i * xOffset)); + mLayout.getChildAt(i).setTranslationY(pos.y + (i * yOffset)); + } + } + } + + public void setStackPosition(BubbleStackView.RelativeStackPosition position) { + setStackPosition(position.getAbsolutePositionInRegion(getAllowableStackPositionRegion())); + } + + private boolean isStackPositionSet() { + return mStackMovedToStartPosition; + } + + /** Animates in the given bubble. */ + private void animateInBubble(View v, int index) { + if (!isActiveController()) { + return; + } + v.setTranslationX(mStackPosition.x); + final float yOffset = + getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_Y); + final float endY = mStackPosition.y + yOffset * index; + final float startY = endY + NEW_BUBBLE_START_Y; + v.setTranslationY(startY); + v.setScaleX(NEW_BUBBLE_START_SCALE); + v.setScaleY(NEW_BUBBLE_START_SCALE); + v.setAlpha(0f); + final ViewPropertyAnimator animator = v.animate() + .translationY(endY) + .scaleX(1f) + .scaleY(1f) + .alpha(1f) + .setDuration(BUBBLE_SWAP_DURATION) + .withEndAction(() -> { + v.setTag(R.id.reorder_animator_tag, null); + }); + v.setTag(R.id.reorder_animator_tag, animator); + } + + /** + * Cancels any outstanding first bubble property animations that are running. This does not + * affect the SpringAnimations controlling the individual bubbles' 'following' effect - it only + * cancels animations started from {@link #springFirstBubbleWithStackFollowing} and + * {@link #flingThenSpringFirstBubbleWithStackFollowing}. + */ + private void cancelStackPositionAnimation(DynamicAnimation.ViewProperty property) { + if (mStackPositionAnimations.containsKey(property)) { + mStackPositionAnimations.get(property).cancel(); + } + } + + /** + * Returns the {@link MagnetizedObject} instance for the bubble stack, with the provided + * {@link MagnetizedObject.MagneticTarget} added as a target. + */ + public MagnetizedObject<StackAnimationController> getMagnetizedStack( + MagnetizedObject.MagneticTarget target) { + if (mMagnetizedStack == null) { + mMagnetizedStack = new MagnetizedObject<StackAnimationController>( + mLayout.getContext(), + this, + new StackPositionProperty(DynamicAnimation.TRANSLATION_X), + new StackPositionProperty(DynamicAnimation.TRANSLATION_Y) + ) { + @Override + public float getWidth(@NonNull StackAnimationController underlyingObject) { + return mBubbleSize; + } + + @Override + public float getHeight(@NonNull StackAnimationController underlyingObject) { + return mBubbleSize; + } + + @Override + public void getLocationOnScreen(@NonNull StackAnimationController underlyingObject, + @NonNull int[] loc) { + loc[0] = (int) mStackPosition.x; + loc[1] = (int) mStackPosition.y; + } + }; + mMagnetizedStack.addTarget(target); + mMagnetizedStack.setHapticsEnabled(true); + mMagnetizedStack.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY); + } + + final ContentResolver contentResolver = mLayout.getContext().getContentResolver(); + final float minVelocity = Settings.Secure.getFloat(contentResolver, + "bubble_dismiss_fling_min_velocity", + mMagnetizedStack.getFlingToTargetMinVelocity() /* default */); + final float maxVelocity = Settings.Secure.getFloat(contentResolver, + "bubble_dismiss_stick_max_velocity", + mMagnetizedStack.getStickToTargetMaxXVelocity() /* default */); + final float targetWidth = Settings.Secure.getFloat(contentResolver, + "bubble_dismiss_target_width_percent", + mMagnetizedStack.getFlingToTargetWidthPercent() /* default */); + + mMagnetizedStack.setFlingToTargetMinVelocity(minVelocity); + mMagnetizedStack.setStickToTargetMaxXVelocity(maxVelocity); + mMagnetizedStack.setFlingToTargetWidthPercent(targetWidth); + + return mMagnetizedStack; + } + + /** Returns the number of 'real' bubbles (excluding overflow). */ + private int getBubbleCount() { + return mBubbleCountSupplier.getAsInt(); + } + + /** + * FloatProperty that uses {@link #moveFirstBubbleWithStackFollowing} to set the first bubble's + * translation and animate the rest of the stack with it. A DynamicAnimation can animate this + * property directly to move the first bubble and cause the stack to 'follow' to the new + * location. + * + * This could also be achieved by simply animating the first bubble view and adding an update + * listener to dispatch movement to the rest of the stack. However, this would require + * duplication of logic in that update handler - it's simpler to keep all logic contained in the + * {@link #moveFirstBubbleWithStackFollowing} method. + */ + private class StackPositionProperty + extends FloatPropertyCompat<StackAnimationController> { + private final DynamicAnimation.ViewProperty mProperty; + + private StackPositionProperty(DynamicAnimation.ViewProperty property) { + super(property.toString()); + mProperty = property; + } + + @Override + public float getValue(StackAnimationController controller) { + return mLayout.getChildCount() > 0 ? mProperty.getValue(mLayout.getChildAt(0)) : 0; + } + + @Override + public void setValue(StackAnimationController controller, float value) { + moveFirstBubbleWithStackFollowing(mProperty, value); + } + } +} + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleEntity.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleEntity.kt new file mode 100644 index 000000000000..aeba302bf487 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleEntity.kt @@ -0,0 +1,29 @@ +/* + * 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.bubbles.storage + +import android.annotation.DimenRes +import android.annotation.UserIdInt + +data class BubbleEntity( + @UserIdInt val userId: Int, + val packageName: String, + val shortcutId: String, + val key: String, + val desiredHeight: Int, + @DimenRes val desiredHeightResId: Int, + val title: String? = null +) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepository.kt new file mode 100644 index 000000000000..66a75af7d64c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepository.kt @@ -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.bubbles.storage + +import android.content.Context +import android.util.AtomicFile +import android.util.Log +import java.io.File +import java.io.FileOutputStream +import java.io.IOException + +class BubblePersistentRepository(context: Context) { + + private val bubbleFile: AtomicFile = AtomicFile(File(context.filesDir, + "overflow_bubbles.xml"), "overflow-bubbles") + + fun persistsToDisk(bubbles: List<BubbleEntity>): Boolean { + if (DEBUG) Log.d(TAG, "persisting ${bubbles.size} bubbles") + synchronized(bubbleFile) { + val stream: FileOutputStream = try { bubbleFile.startWrite() } catch (e: IOException) { + Log.e(TAG, "Failed to save bubble file", e) + return false + } + try { + writeXml(stream, bubbles) + bubbleFile.finishWrite(stream) + if (DEBUG) Log.d(TAG, "persisted ${bubbles.size} bubbles") + return true + } catch (e: Exception) { + Log.e(TAG, "Failed to save bubble file, restoring backup", e) + bubbleFile.failWrite(stream) + } + } + return false + } + + fun readFromDisk(): List<BubbleEntity> { + synchronized(bubbleFile) { + if (!bubbleFile.exists()) return emptyList() + try { return bubbleFile.openRead().use(::readXml) } catch (e: Throwable) { + Log.e(TAG, "Failed to open bubble file", e) + } + return emptyList() + } + } +} + +private const val TAG = "BubblePersistentRepository" +private const val DEBUG = false diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepository.kt new file mode 100644 index 000000000000..7f0b165bdc25 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepository.kt @@ -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.bubbles.storage + +import android.content.pm.LauncherApps +import android.os.UserHandle +import com.android.internal.annotations.VisibleForTesting +import com.android.wm.shell.bubbles.ShortcutKey + +private const val CAPACITY = 16 + +/** + * BubbleVolatileRepository holds the most updated snapshot of list of bubbles for in-memory + * manipulation. + */ +class BubbleVolatileRepository(private val launcherApps: LauncherApps) { + /** + * An ordered set of bubbles based on their natural ordering. + */ + private var entities = mutableSetOf<BubbleEntity>() + + /** + * The capacity of the cache. + */ + @VisibleForTesting + var capacity = CAPACITY + + /** + * Returns a snapshot of all the bubbles. + */ + val bubbles: List<BubbleEntity> + @Synchronized + get() = entities.toList() + + /** + * Add the bubbles to memory and perform a de-duplication. In case a bubble already exists, + * it will be moved to the last. + */ + @Synchronized + fun addBubbles(bubbles: List<BubbleEntity>) { + if (bubbles.isEmpty()) return + // Verify the size of given bubbles is within capacity, otherwise trim down to capacity + val bubblesInRange = bubbles.takeLast(capacity) + // To ensure natural ordering of the bubbles, removes bubbles which already exist + val uniqueBubbles = bubblesInRange.filterNot { b: BubbleEntity -> + entities.removeIf { e: BubbleEntity -> b.key == e.key } } + val overflowCount = entities.size + bubblesInRange.size - capacity + if (overflowCount > 0) { + // Uncache ShortcutInfo of bubbles that will be removed due to capacity + uncache(entities.take(overflowCount)) + entities = entities.drop(overflowCount).toMutableSet() + } + entities.addAll(bubblesInRange) + cache(uniqueBubbles) + } + + @Synchronized + fun removeBubbles(bubbles: List<BubbleEntity>) = + uncache(bubbles.filter { b: BubbleEntity -> + entities.removeIf { e: BubbleEntity -> b.key == e.key } }) + + private fun cache(bubbles: List<BubbleEntity>) { + bubbles.groupBy { ShortcutKey(it.userId, it.packageName) }.forEach { (key, bubbles) -> + launcherApps.cacheShortcuts(key.pkg, bubbles.map { it.shortcutId }, + UserHandle.of(key.userId), LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS) + } + } + + private fun uncache(bubbles: List<BubbleEntity>) { + bubbles.groupBy { ShortcutKey(it.userId, it.packageName) }.forEach { (key, bubbles) -> + launcherApps.uncacheShortcuts(key.pkg, bubbles.map { it.shortcutId }, + UserHandle.of(key.userId), LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS) + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelper.kt new file mode 100644 index 000000000000..fe72bd301e04 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelper.kt @@ -0,0 +1,115 @@ +/* + * 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.bubbles.storage + +import android.util.Xml +import com.android.internal.util.FastXmlSerializer +import com.android.internal.util.XmlUtils +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlSerializer +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.nio.charset.StandardCharsets + +// TODO: handle version changes gracefully +private const val CURRENT_VERSION = 1 + +private const val TAG_BUBBLES = "bs" +private const val ATTR_VERSION = "v" +private const val TAG_BUBBLE = "bb" +private const val ATTR_USER_ID = "uid" +private const val ATTR_PACKAGE = "pkg" +private const val ATTR_SHORTCUT_ID = "sid" +private const val ATTR_KEY = "key" +private const val ATTR_DESIRED_HEIGHT = "h" +private const val ATTR_DESIRED_HEIGHT_RES_ID = "hid" +private const val ATTR_TITLE = "t" + +/** + * Writes the bubbles in xml format into given output stream. + */ +@Throws(IOException::class) +fun writeXml(stream: OutputStream, bubbles: List<BubbleEntity>) { + val serializer: XmlSerializer = FastXmlSerializer() + serializer.setOutput(stream, StandardCharsets.UTF_8.name()) + serializer.startDocument(null, true) + serializer.startTag(null, TAG_BUBBLES) + serializer.attribute(null, ATTR_VERSION, CURRENT_VERSION.toString()) + bubbles.forEach { b -> writeXmlEntry(serializer, b) } + serializer.endTag(null, TAG_BUBBLES) + serializer.endDocument() +} + +/** + * Creates a xml entry for given bubble in following format: + * ``` + * <bb uid="0" pkg="com.example.messenger" sid="my-shortcut" key="my-key" /> + * ``` + */ +private fun writeXmlEntry(serializer: XmlSerializer, bubble: BubbleEntity) { + try { + serializer.startTag(null, TAG_BUBBLE) + serializer.attribute(null, ATTR_USER_ID, bubble.userId.toString()) + serializer.attribute(null, ATTR_PACKAGE, bubble.packageName) + serializer.attribute(null, ATTR_SHORTCUT_ID, bubble.shortcutId) + serializer.attribute(null, ATTR_KEY, bubble.key) + serializer.attribute(null, ATTR_DESIRED_HEIGHT, bubble.desiredHeight.toString()) + serializer.attribute(null, ATTR_DESIRED_HEIGHT_RES_ID, bubble.desiredHeightResId.toString()) + bubble.title?.let { serializer.attribute(null, ATTR_TITLE, it) } + serializer.endTag(null, TAG_BUBBLE) + } catch (e: IOException) { + throw RuntimeException(e) + } +} + +/** + * Reads the bubbles from xml file. + */ +fun readXml(stream: InputStream): List<BubbleEntity> { + val bubbles = mutableListOf<BubbleEntity>() + val parser: XmlPullParser = Xml.newPullParser() + parser.setInput(stream, StandardCharsets.UTF_8.name()) + XmlUtils.beginDocument(parser, TAG_BUBBLES) + val version = parser.getAttributeWithName(ATTR_VERSION)?.toInt() + if (version != null && version == CURRENT_VERSION) { + val outerDepth = parser.depth + while (XmlUtils.nextElementWithin(parser, outerDepth)) { + bubbles.add(readXmlEntry(parser) ?: continue) + } + } + return bubbles +} + +private fun readXmlEntry(parser: XmlPullParser): BubbleEntity? { + while (parser.eventType != XmlPullParser.START_TAG) { parser.next() } + return BubbleEntity( + parser.getAttributeWithName(ATTR_USER_ID)?.toInt() ?: return null, + parser.getAttributeWithName(ATTR_PACKAGE) ?: return null, + parser.getAttributeWithName(ATTR_SHORTCUT_ID) ?: return null, + parser.getAttributeWithName(ATTR_KEY) ?: return null, + parser.getAttributeWithName(ATTR_DESIRED_HEIGHT)?.toInt() ?: return null, + parser.getAttributeWithName(ATTR_DESIRED_HEIGHT_RES_ID)?.toInt() ?: return null, + parser.getAttributeWithName(ATTR_TITLE) + ) +} + +private fun XmlPullParser.getAttributeWithName(name: String): String? { + for (i in 0 until attributeCount) { + if (getAttributeName(i) == name) return getAttributeValue(i) + } + return null +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/AlphaOptimizedButton.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/AlphaOptimizedButton.java new file mode 100644 index 000000000000..6f0a61b3187f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/AlphaOptimizedButton.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.content.Context; +import android.util.AttributeSet; +import android.widget.Button; + +/** + * A Button which doesn't have overlapping drawing commands + * + * This is the copy from SystemUI/statusbar. + */ +public class AlphaOptimizedButton extends Button { + public AlphaOptimizedButton(Context context) { + super(context); + } + + public AlphaOptimizedButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AlphaOptimizedButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public AlphaOptimizedButton(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } +} 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..3a7b534f3c17 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java @@ -0,0 +1,122 @@ +/* + * 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.RemoteException; +import android.view.IDisplayWindowRotationCallback; +import android.view.IDisplayWindowRotationController; +import android.view.IWindowManager; +import android.window.WindowContainerTransaction; + +import androidx.annotation.BinderThread; + +import com.android.wm.shell.common.annotations.ShellMainThread; + +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 ShellExecutor mMainExecutor; + private final IWindowManager mWmService; + private final IDisplayWindowRotationController mControllerImpl; + + private final ArrayList<OnDisplayChangingListener> mRotationListener = + new ArrayList<>(); + private final ArrayList<OnDisplayChangingListener> mTmpListeners = new ArrayList<>(); + + public DisplayChangeController(IWindowManager wmService, ShellExecutor mainExecutor) { + mMainExecutor = mainExecutor; + mWmService = wmService; + mControllerImpl = new DisplayWindowRotationControllerImpl(); + try { + mWmService.setDisplayWindowRotationController(mControllerImpl); + } 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); + } + } + + private void onRotateDisplay(int displayId, final int fromRotation, final int toRotation, + IDisplayWindowRotationCallback callback) { + 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) { + } + } + + @BinderThread + private class DisplayWindowRotationControllerImpl + extends IDisplayWindowRotationController.Stub { + @Override + public void onRotateDisplay(int displayId, final int fromRotation, + final int toRotation, IDisplayWindowRotationCallback callback) { + mMainExecutor.execute(() -> { + DisplayChangeController.this.onRotateDisplay(displayId, fromRotation, toRotation, + callback); + }); + } + } + + /** + * 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. + */ + @ShellMainThread + 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..ba9ba5e5883a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java @@ -0,0 +1,298 @@ +/* + * 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.RemoteException; +import android.util.Slog; +import android.util.SparseArray; +import android.view.Display; +import android.view.IDisplayWindowListener; +import android.view.IWindowManager; + +import androidx.annotation.BinderThread; + +import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener; +import com.android.wm.shell.common.annotations.ShellMainThread; + +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 ShellExecutor mMainExecutor; + private final Context mContext; + private final IWindowManager mWmService; + private final DisplayChangeController mChangeController; + private final IDisplayWindowListener mDisplayContainerListener; + + 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); + } + + public DisplayController(Context context, IWindowManager wmService, + ShellExecutor mainExecutor) { + mMainExecutor = mainExecutor; + mContext = context; + mWmService = wmService; + mChangeController = new DisplayChangeController(mWmService, mainExecutor); + mDisplayContainerListener = new DisplayWindowListenerImpl(); + 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 void onDisplayAdded(int displayId) { + 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); + } + } + } + + private void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + 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); + } + } + } + + private void onDisplayRemoved(int displayId) { + 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); + } + } + + private void onFixedRotationStarted(int displayId, int newRotation) { + 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); + } + } + } + + private void onFixedRotationFinished(int displayId) { + 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); + } + } + } + + private static class DisplayRecord { + int mDisplayId; + Context mContext; + DisplayLayout mDisplayLayout; + } + + @BinderThread + private class DisplayWindowListenerImpl extends IDisplayWindowListener.Stub { + @Override + public void onDisplayAdded(int displayId) { + mMainExecutor.execute(() -> { + DisplayController.this.onDisplayAdded(displayId); + }); + } + + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + mMainExecutor.execute(() -> { + DisplayController.this.onDisplayConfigurationChanged(displayId, newConfig); + }); + } + + @Override + public void onDisplayRemoved(int displayId) { + mMainExecutor.execute(() -> { + DisplayController.this.onDisplayRemoved(displayId); + }); + } + + @Override + public void onFixedRotationStarted(int displayId, int newRotation) { + mMainExecutor.execute(() -> { + DisplayController.this.onFixedRotationStarted(displayId, newRotation); + }); + } + + @Override + public void onFixedRotationFinished(int displayId) { + mMainExecutor.execute(() -> { + DisplayController.this.onFixedRotationFinished(displayId); + }); + } + } + + /** + * Gets notified when a display is added/removed to the WM hierarchy and when a display's + * window-configuration changes. + * + * @see IDisplayWindowListener + */ + @ShellMainThread + 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..b2ac61cf3f6e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java @@ -0,0 +1,612 @@ +/* + * 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.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 androidx.annotation.BinderThread; +import androidx.annotation.VisibleForTesting; + +import com.android.internal.inputmethod.Completable; +import com.android.internal.inputmethod.ResultCallbacks; +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 mMainExecutor; + 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) { + mWmService = wmService; + mDisplayController = displayController; + mMainExecutor = mainExecutor; + mTransactionPool = transactionPool; + } + + /** 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()); + pd.register(); + 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); + } + } + } + + private void dispatchVisibilityChanged(int displayId, boolean isShowing) { + synchronized (mPositionProcessors) { + for (ImePositionProcessor pp : mPositionProcessors) { + pp.onImeVisibilityChanged(displayId, isShowing); + } + } + } + + /** + * 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 { + final int mDisplayId; + final InsetsState mInsetsState = new InsetsState(); + protected final DisplayWindowInsetsControllerImpl mInsetsControllerImpl = + new DisplayWindowInsetsControllerImpl(); + 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; + } + + public void register() { + try { + mWmService.setDisplayWindowInsetsController(mDisplayId, mInsetsControllerImpl); + } catch (RemoteException e) { + Slog.w(TAG, "Unable to set insets controller on display " + mDisplayId); + } + } + + protected void insetsChanged(InsetsState insetsState) { + if (mInsetsState.equals(insetsState)) { + return; + } + + updateImeVisibility(insetsState.getSourceOrDefaultVisibility(InsetsState.ITYPE_IME)); + + 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 */); + } + } + + @VisibleForTesting + protected 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) { + 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); + } + } + + protected void showInsets(int types, boolean fromIme) { + if ((types & WindowInsets.Type.ime()) == 0) { + return; + } + if (DEBUG) Slog.d(TAG, "Got showInsets for ime"); + startAnimation(true /* show */, false /* forceRestart */); + } + + + protected void hideInsets(int types, boolean fromIme) { + if ((types & WindowInsets.Type.ime()) == 0) { + return; + } + if (DEBUG) Slog.d(TAG, "Got hideInsets for ime"); + startAnimation(false /* show */, false /* forceRestart */); + } + + public void topFocusedWindowChanged(String packageName) { + // Do nothing + } + + /** + * 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; + updateImeVisibility(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 */); + } + } + + private void updateImeVisibility(boolean isShowing) { + if (mImeShowing != isShowing) { + mImeShowing = isShowing; + dispatchVisibilityChanged(mDisplayId, isShowing); + } + } + + @VisibleForTesting + @BinderThread + public class DisplayWindowInsetsControllerImpl + extends IDisplayWindowInsetsController.Stub { + @Override + public void topFocusedWindowChanged(String packageName) throws RemoteException { + mMainExecutor.execute(() -> { + PerDisplay.this.topFocusedWindowChanged(packageName); + }); + } + + @Override + public void insetsChanged(InsetsState insetsState) throws RemoteException { + mMainExecutor.execute(() -> { + PerDisplay.this.insetsChanged(insetsState); + }); + } + + @Override + public void insetsControlChanged(InsetsState insetsState, + InsetsSourceControl[] activeControls) throws RemoteException { + mMainExecutor.execute(() -> { + PerDisplay.this.insetsControlChanged(insetsState, activeControls); + }); + } + + @Override + public void showInsets(int types, boolean fromIme) throws RemoteException { + mMainExecutor.execute(() -> { + PerDisplay.this.showInsets(types, fromIme); + }); + } + + @Override + public void hideInsets(int types, boolean fromIme) throws RemoteException { + mMainExecutor.execute(() -> { + PerDisplay.this.hideInsets(types, fromIme); + }); + } + } + } + + void removeImeSurface() { + final IInputMethodManager imms = getImms(); + if (imms != null) { + try { + // Remove the IME surface to make the insets invisible for + // non-client controlled insets. + final Completable.Void value = Completable.createVoid(); + imms.removeImeSurface(ResultCallbacks.of(value)); + Completable.getResult(value); + } 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) { + } + + /** + * Called when the IME visibility changed. + * + * @param isShowing {@code true} if the IME is shown. + */ + default void onImeVisibilityChanged(int displayId, boolean isShowing) { + + } + } + + 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..58a4baf39614 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java @@ -0,0 +1,515 @@ +/* + * 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; + } + if (rotation == ROTATION_0) { + return computeSafeInsets(cutout, displayWidth, displayHeight); + } + final Insets waterfallInsets = + RotationUtils.rotateInsets(cutout.getWaterfallInsets(), rotation); + 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; + } + final DisplayCutout.CutoutPathParserInfo info = cutout.getCutoutPathParserInfo(); + final DisplayCutout.CutoutPathParserInfo newInfo = new DisplayCutout.CutoutPathParserInfo( + info.getDisplayWidth(), info.getDisplayHeight(), info.getDensity(), + info.getCutoutSpec(), rotation, info.getScale()); + return computeSafeInsets( + DisplayCutout.constructDisplayCutout(newBounds, waterfallInsets, newInfo), + 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..a4cd3c5a583d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.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.common; + +import android.annotation.NonNull; +import android.os.Handler; +import android.os.Looper; + +/** 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 execute(@NonNull Runnable command) { + if (mHandler.getLooper().isCurrentThread()) { + command.run(); + return; + } + if (!mHandler.post(command)) { + throw new RuntimeException(mHandler + " is probably exiting"); + } + } + + @Override + public void executeDelayed(@NonNull Runnable r, long delayMillis) { + if (!mHandler.postDelayed(r, delayMillis)) { + throw new RuntimeException(mHandler + " is probably exiting"); + } + } + + @Override + public void removeAllCallbacks() { + mHandler.removeCallbacksAndMessages(null); + } + + @Override + public void removeCallbacks(@NonNull Runnable r) { + mHandler.removeCallbacks(r); + } + + @Override + public boolean hasCallback(Runnable r) { + return mHandler.hasCallbacks(r); + } +} 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..6abc8f6dda89 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.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.common; + +import android.os.Looper; +import android.os.SystemClock; +import android.os.Trace; + +import java.lang.reflect.Array; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.function.BooleanSupplier; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/** + * 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 { + + /** + * Executes the given runnable. If the caller is running on the same looper as this executor, + * the runnable must be executed immediately. + */ + @Override + void execute(Runnable runnable); + + /** + * Executes the given runnable in a blocking call. If the caller is running on the same looper + * as this executor, the runnable must be executed immediately. + * + * @throws InterruptedException if runnable does not return in the time specified by + * {@param waitTimeout} + */ + default void executeBlocking(Runnable runnable, int waitTimeout, TimeUnit waitTimeUnit) + throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + execute(() -> { + runnable.run(); + latch.countDown(); + }); + latch.await(waitTimeout, waitTimeUnit); + } + + /** + * Convenience method to execute the blocking call with a default timeout. + * + * @throws InterruptedException if runnable does not return in the time specified by + * {@param waitTimeout} + */ + default void executeBlocking(Runnable runnable) throws InterruptedException { + executeBlocking(runnable, 2, TimeUnit.SECONDS); + } + + /** + * Convenience method to execute the blocking call with a default timeout and returns a value. + * Waits indefinitely for a typed result from a call. + */ + default <T> T executeBlockingForResult(Supplier<T> runnable, Class clazz) { + final T[] result = (T[]) Array.newInstance(clazz, 1); + final CountDownLatch latch = new CountDownLatch(1); + execute(() -> { + result[0] = runnable.get(); + latch.countDown(); + }); + try { + latch.await(); + return result[0]; + } catch (InterruptedException e) { + return null; + } + } + + + /** + * See {@link android.os.Handler#postDelayed(Runnable, long)}. + */ + void executeDelayed(Runnable runnable, long delayMillis); + + /** + * Removes all pending callbacks. + */ + void removeAllCallbacks(); + + /** + * See {@link android.os.Handler#removeCallbacks}. + */ + void removeCallbacks(Runnable runnable); + + /** + * See {@link android.os.Handler#hasCallbacks(Runnable)}. + */ + boolean hasCallback(Runnable runnable); +} 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..33beab5ee3f1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java @@ -0,0 +1,179 @@ +/* + * 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.BinderThread; +import android.annotation.NonNull; +import android.util.Slog; +import android.view.SurfaceControl; +import android.window.WindowContainerTransaction; +import android.window.WindowContainerTransactionCallback; +import android.window.WindowOrganizer; + +import com.android.wm.shell.common.annotations.ShellMainThread; + +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 ShellExecutor mMainExecutor; + + // 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, ShellExecutor mainExecutor) { + mTransactionPool = pool; + mMainExecutor = mainExecutor; + } + + /** + * 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); + mMainExecutor.executeDelayed(mOnReplyTimeout, REPLY_TIMEOUT); + } + + @BinderThread + @Override + public void onTransactionReady(int id, + @NonNull SurfaceControl.Transaction t) { + mMainExecutor.execute(() -> { + synchronized (mQueue) { + if (mId != id) { + Slog.e(TAG, "Got an unexpected onTransactionReady. Expected " + + mId + " but got " + id); + return; + } + mInFlight = null; + mMainExecutor.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..616f24a874bc --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java @@ -0,0 +1,350 @@ +/* + * 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, + @WindowManager.ShellRootLayer int shellRootLayer) { + PerDisplay pd = mPerDisplay.get(displayId); + if (pd == null) { + pd = new PerDisplay(displayId); + mPerDisplay.put(displayId, pd); + } + pd.addView(view, attrs, shellRootLayer); + } + + /** + * 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); + } + + /** + * 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, + @WindowManager.ShellRootLayer int shellRootLayer) { + SysUiWindowManager wwm = addRoot(shellRootLayer); + 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, shellRootLayer, + viewRoot.getWindowToken()); + } catch (RemoteException e) { + Slog.e(TAG, "Error setting accessibility window for " + mDisplayId + ":" + + shellRootLayer, e); + } + } + SysUiWindowManager addRoot(@WindowManager.ShellRootLayer int shellRootLayer) { + SysUiWindowManager wwm = mWwms.get(shellRootLayer); + if (wwm != null) { + return wwm; + } + SurfaceControl rootSurface = null; + ContainerWindow win = new ContainerWindow(); + try { + rootSurface = mWmService.addShellRoot(mDisplayId, win, shellRootLayer); + } 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(shellRootLayer, 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 requestScrollCapture(IScrollCaptureCallbacks callbacks) { + try { + callbacks.onUnavailable(); + } catch (RemoteException ex) { + // ignore + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java new file mode 100644 index 000000000000..59374a6069c8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java @@ -0,0 +1,87 @@ +/* + * 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.app.ActivityManager.RunningTaskInfo; +import android.app.ITaskStackListener; +import android.content.ComponentName; +import android.window.TaskSnapshot; + +import androidx.annotation.BinderThread; + +/** + * An interface to track task stack changes. Classes should implement this instead of + * {@link ITaskStackListener} to reduce IPC calls from system services. + */ +public interface TaskStackListenerCallback { + + default void onRecentTaskListUpdated() { } + + default void onRecentTaskListFrozenChanged(boolean frozen) { } + + @BinderThread + default void onTaskStackChangedBackground() { } + + default void onTaskStackChanged() { } + + default void onTaskProfileLocked(int taskId, int userId) { } + + default void onTaskDisplayChanged(int taskId, int newDisplayId) { } + + default void onTaskCreated(int taskId, ComponentName componentName) { } + + default void onTaskRemoved(int taskId) { } + + default void onTaskMovedToFront(int taskId) { } + + default void onTaskMovedToFront(RunningTaskInfo taskInfo) { + onTaskMovedToFront(taskInfo.taskId); + } + + default void onTaskDescriptionChanged(RunningTaskInfo taskInfo) { } + + default void onTaskSnapshotChanged(int taskId, TaskSnapshot snapshot) { } + + default void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) { } + + default void onActivityRestartAttempt(RunningTaskInfo task, boolean homeTaskVisible, + boolean clearedTask, boolean wasVisible) { } + + default void onActivityPinned(String packageName, int userId, int taskId, int stackId) { } + + default void onActivityUnpinned() { } + + default void onActivityForcedResizable(String packageName, int taskId, int reason) { } + + default void onActivityDismissingDockedStack() { } + + default void onActivityLaunchOnSecondaryDisplayFailed() { } + + default void onActivityLaunchOnSecondaryDisplayFailed(RunningTaskInfo taskInfo) { + onActivityLaunchOnSecondaryDisplayFailed(); + } + + default void onActivityLaunchOnSecondaryDisplayRerouted() { } + + default void onActivityLaunchOnSecondaryDisplayRerouted(RunningTaskInfo taskInfo) { + onActivityLaunchOnSecondaryDisplayRerouted(); + } + + default void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) { } + + default void onActivityRotation(int displayId) { } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java new file mode 100644 index 000000000000..e94080aa8db7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java @@ -0,0 +1,424 @@ +/* + * 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.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.IActivityTaskManager; +import android.app.TaskStackListener; +import android.content.ComponentName; +import android.os.Handler; +import android.os.Message; +import android.os.Trace; +import android.util.Log; +import android.window.TaskSnapshot; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.SomeArgs; + +import java.util.ArrayList; +import java.util.List; + +/** + * Implementation of a {@link android.app.TaskStackListener}. + */ +public class TaskStackListenerImpl extends TaskStackListener implements Handler.Callback { + private static final String TAG = TaskStackListenerImpl.class.getSimpleName(); + + private static final int ON_TASK_STACK_CHANGED = 1; + private static final int ON_TASK_SNAPSHOT_CHANGED = 2; + private static final int ON_ACTIVITY_PINNED = 3; + private static final int ON_ACTIVITY_RESTART_ATTEMPT = 4; + private static final int ON_ACTIVITY_FORCED_RESIZABLE = 5; + private static final int ON_ACTIVITY_DISMISSING_DOCKED_STACK = 6; + private static final int ON_TASK_PROFILE_LOCKED = 7; + private static final int ON_ACTIVITY_UNPINNED = 8; + private static final int ON_ACTIVITY_LAUNCH_ON_SECONDARY_DISPLAY_FAILED = 9; + private static final int ON_TASK_CREATED = 10; + private static final int ON_TASK_REMOVED = 11; + private static final int ON_TASK_MOVED_TO_FRONT = 12; + private static final int ON_ACTIVITY_REQUESTED_ORIENTATION_CHANGE = 13; + private static final int ON_ACTIVITY_LAUNCH_ON_SECONDARY_DISPLAY_REROUTED = 14; + private static final int ON_BACK_PRESSED_ON_TASK_ROOT = 15; + private static final int ON_TASK_DISPLAY_CHANGED = 16; + private static final int ON_TASK_LIST_UPDATED = 17; + private static final int ON_TASK_LIST_FROZEN_UNFROZEN = 18; + private static final int ON_TASK_DESCRIPTION_CHANGED = 19; + private static final int ON_ACTIVITY_ROTATION = 20; + + /** + * List of {@link TaskStackListenerCallback} registered from {@link #addListener}. + */ + private final List<TaskStackListenerCallback> mTaskStackListeners = new ArrayList<>(); + private final List<TaskStackListenerCallback> mTmpListeners = new ArrayList<>(); + + private final IActivityTaskManager mActivityTaskManager; + // NOTE: In this case we do want to use a handler since we rely on the message system to + // efficiently dedupe sequential calls + private Handler mMainHandler; + + public TaskStackListenerImpl(Handler mainHandler) { + mActivityTaskManager = ActivityTaskManager.getService(); + mMainHandler = new Handler(mainHandler.getLooper(), this); + } + + @VisibleForTesting + TaskStackListenerImpl(IActivityTaskManager activityTaskManager) { + mActivityTaskManager = activityTaskManager; + } + + @VisibleForTesting + void setHandler(Handler mainHandler) { + mMainHandler = mainHandler; + } + + public void addListener(TaskStackListenerCallback listener) { + final boolean wasEmpty; + synchronized (mTaskStackListeners) { + wasEmpty = mTaskStackListeners.isEmpty(); + mTaskStackListeners.add(listener); + } + if (wasEmpty) { + // Register mTaskStackListener to IActivityManager only once if needed. + try { + mActivityTaskManager.registerTaskStackListener(this); + } catch (Exception e) { + Log.w(TAG, "Failed to call registerTaskStackListener", e); + } + } + } + + public void removeListener(TaskStackListenerCallback listener) { + final boolean wasEmpty; + final boolean isEmpty; + synchronized (mTaskStackListeners) { + wasEmpty = mTaskStackListeners.isEmpty(); + mTaskStackListeners.remove(listener); + isEmpty = mTaskStackListeners.isEmpty(); + } + if (!wasEmpty && isEmpty) { + // Unregister mTaskStackListener once we have no more listeners + try { + mActivityTaskManager.unregisterTaskStackListener(this); + } catch (Exception e) { + Log.w(TAG, "Failed to call unregisterTaskStackListener", e); + } + } + } + + @Override + public void onRecentTaskListUpdated() { + mMainHandler.obtainMessage(ON_TASK_LIST_UPDATED).sendToTarget(); + } + + @Override + public void onRecentTaskListFrozenChanged(boolean frozen) { + mMainHandler.obtainMessage(ON_TASK_LIST_FROZEN_UNFROZEN, frozen ? 1 : 0, + 0 /* unused */).sendToTarget(); + } + + @Override + public void onTaskStackChanged() { + // Call the task changed callback for the non-ui thread listeners first. Copy to a set + // of temp listeners so that we don't lock on mTaskStackListeners while calling all the + // callbacks. This call is always on the same binder thread, so we can just synchronize + // on the copying of the listener list. + synchronized (mTaskStackListeners) { + mTmpListeners.addAll(mTaskStackListeners); + } + for (int i = mTmpListeners.size() - 1; i >= 0; i--) { + mTmpListeners.get(i).onTaskStackChangedBackground(); + } + mTmpListeners.clear(); + + mMainHandler.removeMessages(ON_TASK_STACK_CHANGED); + mMainHandler.sendEmptyMessage(ON_TASK_STACK_CHANGED); + } + + @Override + public void onTaskProfileLocked(int taskId, int userId) { + mMainHandler.obtainMessage(ON_TASK_PROFILE_LOCKED, taskId, userId).sendToTarget(); + } + + @Override + public void onTaskDisplayChanged(int taskId, int newDisplayId) { + mMainHandler.obtainMessage(ON_TASK_DISPLAY_CHANGED, taskId, + newDisplayId).sendToTarget(); + } + + @Override + public void onTaskCreated(int taskId, ComponentName componentName) { + mMainHandler.obtainMessage(ON_TASK_CREATED, taskId, 0, componentName).sendToTarget(); + } + + @Override + public void onTaskRemoved(int taskId) { + mMainHandler.obtainMessage(ON_TASK_REMOVED, taskId, 0).sendToTarget(); + } + + @Override + public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) { + mMainHandler.obtainMessage(ON_TASK_MOVED_TO_FRONT, taskInfo).sendToTarget(); + } + + @Override + public void onTaskDescriptionChanged(ActivityManager.RunningTaskInfo taskInfo) { + mMainHandler.obtainMessage(ON_TASK_DESCRIPTION_CHANGED, taskInfo).sendToTarget(); + } + + @Override + public void onTaskSnapshotChanged(int taskId, TaskSnapshot snapshot) { + mMainHandler.obtainMessage(ON_TASK_SNAPSHOT_CHANGED, taskId, 0, snapshot) + .sendToTarget(); + } + + @Override + public void onBackPressedOnTaskRoot(ActivityManager.RunningTaskInfo taskInfo) { + mMainHandler.obtainMessage(ON_BACK_PRESSED_ON_TASK_ROOT, taskInfo).sendToTarget(); + } + + @Override + public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = packageName; + args.argi1 = userId; + args.argi2 = taskId; + args.argi3 = stackId; + mMainHandler.removeMessages(ON_ACTIVITY_PINNED); + mMainHandler.obtainMessage(ON_ACTIVITY_PINNED, args).sendToTarget(); + } + + @Override + public void onActivityUnpinned() { + mMainHandler.removeMessages(ON_ACTIVITY_UNPINNED); + mMainHandler.sendEmptyMessage(ON_ACTIVITY_UNPINNED); + } + + @Override + public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, + boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { + final SomeArgs args = SomeArgs.obtain(); + args.arg1 = task; + args.argi1 = homeTaskVisible ? 1 : 0; + args.argi2 = clearedTask ? 1 : 0; + args.argi3 = wasVisible ? 1 : 0; + mMainHandler.removeMessages(ON_ACTIVITY_RESTART_ATTEMPT); + mMainHandler.obtainMessage(ON_ACTIVITY_RESTART_ATTEMPT, args).sendToTarget(); + } + + @Override + public void onActivityForcedResizable(String packageName, int taskId, int reason) { + mMainHandler.obtainMessage(ON_ACTIVITY_FORCED_RESIZABLE, taskId, reason, packageName) + .sendToTarget(); + } + + @Override + public void onActivityDismissingDockedStack() { + mMainHandler.sendEmptyMessage(ON_ACTIVITY_DISMISSING_DOCKED_STACK); + } + + @Override + public void onActivityLaunchOnSecondaryDisplayFailed( + ActivityManager.RunningTaskInfo taskInfo, + int requestedDisplayId) { + mMainHandler.obtainMessage(ON_ACTIVITY_LAUNCH_ON_SECONDARY_DISPLAY_FAILED, + requestedDisplayId, + 0 /* unused */, + taskInfo).sendToTarget(); + } + + @Override + public void onActivityLaunchOnSecondaryDisplayRerouted( + ActivityManager.RunningTaskInfo taskInfo, + int requestedDisplayId) { + mMainHandler.obtainMessage(ON_ACTIVITY_LAUNCH_ON_SECONDARY_DISPLAY_REROUTED, + requestedDisplayId, 0 /* unused */, taskInfo).sendToTarget(); + } + + @Override + public void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) { + mMainHandler.obtainMessage(ON_ACTIVITY_REQUESTED_ORIENTATION_CHANGE, taskId, + requestedOrientation).sendToTarget(); + } + + @Override + public void onActivityRotation(int displayId) { + mMainHandler.obtainMessage(ON_ACTIVITY_ROTATION, displayId, 0 /* unused */) + .sendToTarget(); + } + + @Override + public boolean handleMessage(Message msg) { + synchronized (mTaskStackListeners) { + switch (msg.what) { + case ON_TASK_STACK_CHANGED: { + Trace.beginSection("onTaskStackChanged"); + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onTaskStackChanged(); + } + Trace.endSection(); + break; + } + case ON_TASK_SNAPSHOT_CHANGED: { + Trace.beginSection("onTaskSnapshotChanged"); + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onTaskSnapshotChanged(msg.arg1, + (TaskSnapshot) msg.obj); + } + Trace.endSection(); + break; + } + case ON_ACTIVITY_PINNED: { + final SomeArgs args = (SomeArgs) msg.obj; + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onActivityPinned((String) args.arg1, args.argi1, + args.argi2, args.argi3); + } + break; + } + case ON_ACTIVITY_UNPINNED: { + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onActivityUnpinned(); + } + break; + } + case ON_ACTIVITY_RESTART_ATTEMPT: { + final SomeArgs args = (SomeArgs) msg.obj; + final ActivityManager.RunningTaskInfo + task = (ActivityManager.RunningTaskInfo) args.arg1; + final boolean homeTaskVisible = args.argi1 != 0; + final boolean clearedTask = args.argi2 != 0; + final boolean wasVisible = args.argi3 != 0; + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onActivityRestartAttempt(task, + homeTaskVisible, clearedTask, wasVisible); + } + break; + } + case ON_ACTIVITY_FORCED_RESIZABLE: { + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onActivityForcedResizable( + (String) msg.obj, msg.arg1, msg.arg2); + } + break; + } + case ON_ACTIVITY_DISMISSING_DOCKED_STACK: { + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onActivityDismissingDockedStack(); + } + break; + } + case ON_ACTIVITY_LAUNCH_ON_SECONDARY_DISPLAY_FAILED: { + final ActivityManager.RunningTaskInfo + info = (ActivityManager.RunningTaskInfo) msg.obj; + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i) + .onActivityLaunchOnSecondaryDisplayFailed(info); + } + break; + } + case ON_ACTIVITY_LAUNCH_ON_SECONDARY_DISPLAY_REROUTED: { + final ActivityManager.RunningTaskInfo + info = (ActivityManager.RunningTaskInfo) msg.obj; + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i) + .onActivityLaunchOnSecondaryDisplayRerouted(info); + } + break; + } + case ON_TASK_PROFILE_LOCKED: { + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onTaskProfileLocked(msg.arg1, msg.arg2); + } + break; + } + case ON_TASK_CREATED: { + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onTaskCreated(msg.arg1, + (ComponentName) msg.obj); + } + break; + } + case ON_TASK_REMOVED: { + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onTaskRemoved(msg.arg1); + } + break; + } + case ON_TASK_MOVED_TO_FRONT: { + final ActivityManager.RunningTaskInfo + info = (ActivityManager.RunningTaskInfo) msg.obj; + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onTaskMovedToFront(info); + } + break; + } + case ON_ACTIVITY_REQUESTED_ORIENTATION_CHANGE: { + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i) + .onActivityRequestedOrientationChanged(msg.arg1, msg.arg2); + } + break; + } + case ON_BACK_PRESSED_ON_TASK_ROOT: { + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onBackPressedOnTaskRoot( + (ActivityManager.RunningTaskInfo) msg.obj); + } + break; + } + case ON_TASK_DISPLAY_CHANGED: { + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onTaskDisplayChanged(msg.arg1, msg.arg2); + } + break; + } + case ON_TASK_LIST_UPDATED: { + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onRecentTaskListUpdated(); + } + break; + } + case ON_TASK_LIST_FROZEN_UNFROZEN: { + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onRecentTaskListFrozenChanged( + msg.arg1 != 0); + } + break; + } + case ON_TASK_DESCRIPTION_CHANGED: { + final ActivityManager.RunningTaskInfo + info = (ActivityManager.RunningTaskInfo) msg.obj; + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onTaskDescriptionChanged(info); + } + break; + } + case ON_ACTIVITY_ROTATION: { + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onActivityRotation(msg.arg1); + } + break; + } + } + } + if (msg.obj instanceof SomeArgs) { + ((SomeArgs) msg.obj).recycle(); + } + return true; + } +}
\ No newline at end of file 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/TriangleShape.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TriangleShape.java new file mode 100644 index 000000000000..707919033065 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TriangleShape.java @@ -0,0 +1,77 @@ +/* + * 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.Outline; +import android.graphics.Path; +import android.graphics.drawable.shapes.PathShape; + +import androidx.annotation.NonNull; + +/** + * Wrapper around {@link PathShape} + * that creates a shape with a triangular path (pointing up or down). + * + * This is the copy from SystemUI/recents. + */ +public class TriangleShape extends PathShape { + private Path mTriangularPath; + + public TriangleShape(Path path, float stdWidth, float stdHeight) { + super(path, stdWidth, stdHeight); + mTriangularPath = path; + } + + public static TriangleShape create(float width, float height, boolean isPointingUp) { + Path triangularPath = new Path(); + if (isPointingUp) { + triangularPath.moveTo(0, height); + triangularPath.lineTo(width, height); + triangularPath.lineTo(width / 2, 0); + triangularPath.close(); + } else { + triangularPath.moveTo(0, 0); + triangularPath.lineTo(width / 2, height); + triangularPath.lineTo(width, 0); + triangularPath.close(); + } + return new TriangleShape(triangularPath, width, height); + } + + /** Create an arrow TriangleShape that points to the left or the right */ + public static TriangleShape createHorizontal( + float width, float height, boolean isPointingLeft) { + Path triangularPath = new Path(); + if (isPointingLeft) { + triangularPath.moveTo(0, height / 2); + triangularPath.lineTo(width, height); + triangularPath.lineTo(width, 0); + triangularPath.close(); + } else { + triangularPath.moveTo(0, height); + triangularPath.lineTo(width, height / 2); + triangularPath.lineTo(0, 0); + triangularPath.close(); + } + return new TriangleShape(triangularPath, width, height); + } + + @Override + public void getOutline(@NonNull Outline outline) { + outline.setPath(mTriangularPath); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ChoreographerSfVsync.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ChoreographerSfVsync.java new file mode 100644 index 000000000000..4009ad21b9b8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ChoreographerSfVsync.java @@ -0,0 +1,18 @@ +package com.android.wm.shell.common.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** + * Annotates a method that or qualifies a provider runs aligned to the Choreographer SF vsync + * instead of the app vsync. + */ +@Documented +@Inherited +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface ChoreographerSfVsync {}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalThread.java new file mode 100644 index 000000000000..7560f71d1f98 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalThread.java @@ -0,0 +1,15 @@ +package com.android.wm.shell.common.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** Annotates a method or class that is called from an external thread to the Shell threads. */ +@Documented +@Inherited +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface ExternalThread {}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellAnimationThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellAnimationThread.java new file mode 100644 index 000000000000..0479f8780c79 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellAnimationThread.java @@ -0,0 +1,15 @@ +package com.android.wm.shell.common.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** Annotates a method or qualifies a provider that runs on the Shell animation-thread */ +@Documented +@Inherited +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface ShellAnimationThread {}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellMainThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellMainThread.java new file mode 100644 index 000000000000..423f4ce3bfd4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellMainThread.java @@ -0,0 +1,15 @@ +package com.android.wm.shell.common.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** Annotates a method or qualifies a provider that runs on the Shell main-thread */ +@Documented +@Inherited +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface ShellMainThread {}
\ No newline at end of file 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..9f6dd1f27b62 --- /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(effectId: Int) { + if (hapticsEnabled && systemHapticsEnabled) { + vibrator.vibrate(VibrationEffect.createPredefined(effectId)) + } + } + + /** 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/common/split/DividerHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java new file mode 100644 index 000000000000..218bf47e24aa --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java @@ -0,0 +1,150 @@ +/* + * 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.common.split; + +import static com.android.wm.shell.common.split.DividerView.TOUCH_ANIMATION_DURATION; +import static com.android.wm.shell.common.split.DividerView.TOUCH_RELEASE_ANIMATION_DURATION; + +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; + } + + /** Sets touching state for this handle view. */ + public 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 + ? TOUCH_ANIMATION_DURATION + : 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/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java new file mode 100644 index 000000000000..c27c92961c2b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java @@ -0,0 +1,218 @@ +/* + * 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.split; + +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.SurfaceControlViewHost; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.policy.DividerSnapAlgorithm; +import com.android.wm.shell.R; +import com.android.wm.shell.animation.Interpolators; + +/** + * Stack divider for app pair. + */ +public class DividerView extends FrameLayout implements View.OnTouchListener { + public static final long TOUCH_ANIMATION_DURATION = 150; + public static final long TOUCH_RELEASE_ANIMATION_DURATION = 200; + + private final int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + + private SplitLayout mSplitLayout; + private SurfaceControlViewHost mViewHost; + private DividerHandleView mHandle; + private View mBackground; + private int mTouchElevation; + + private VelocityTracker mVelocityTracker; + private boolean mMoving; + private int mStartPos; + private GestureDetector mDoubleTapDetector; + + public DividerView(@NonNull Context context) { + super(context); + } + + public DividerView(@NonNull Context context, + @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public DividerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public DividerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + /** Sets up essential dependencies of the divider bar. */ + public void setup( + SplitLayout layout, + SurfaceControlViewHost viewHost) { + mSplitLayout = layout; + mViewHost = viewHost; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mHandle = findViewById(R.id.docked_divider_handle); + mBackground = findViewById(R.id.docked_divider_background); + mTouchElevation = getResources().getDimensionPixelSize( + R.dimen.docked_stack_divider_lift_elevation); + mDoubleTapDetector = new GestureDetector(getContext(), new DoubleTapListener()); + setOnTouchListener(this); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (mSplitLayout == null) { + return false; + } + + final int action = event.getAction() & MotionEvent.ACTION_MASK; + final boolean isLandscape = isLandscape(); + // Using raw xy to prevent lost track of motion events while moving divider bar. + final int touchPos = isLandscape ? (int) event.getRawX() : (int) event.getRawY(); + switch (action) { + case MotionEvent.ACTION_DOWN: + mVelocityTracker = VelocityTracker.obtain(); + mVelocityTracker.addMovement(event); + setTouching(); + mStartPos = touchPos; + mMoving = false; + break; + case MotionEvent.ACTION_MOVE: + mVelocityTracker.addMovement(event); + if (!mMoving && Math.abs(touchPos - mStartPos) > mTouchSlop) { + mStartPos = touchPos; + mMoving = true; + } + if (mMoving) { + final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos; + mSplitLayout.updateDivideBounds(position); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mVelocityTracker.addMovement(event); + mVelocityTracker.computeCurrentVelocity(1000 /* units */); + final float velocity = isLandscape + ? mVelocityTracker.getXVelocity() + : mVelocityTracker.getYVelocity(); + releaseTouching(); + mMoving = false; + + final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos; + final DividerSnapAlgorithm.SnapTarget snapTarget = + mSplitLayout.findSnapTarget(position, velocity, false /* hardDismiss */); + mSplitLayout.snapToTarget(position, snapTarget); + break; + } + + mDoubleTapDetector.onTouchEvent(event); + return true; + } + + private void setTouching() { + setSlippery(false); + mHandle.setTouching(true, true); + if (isLandscape()) { + mBackground.animate().scaleX(1.4f); + } else { + mBackground.animate().scaleY(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(); + } + + private void releaseTouching() { + setSlippery(true); + mHandle.setTouching(false, true); + 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(); + } + + private void setSlippery(boolean slippery) { + if (mViewHost == null) { + return; + } + + final WindowManager.LayoutParams lp = (WindowManager.LayoutParams) getLayoutParams(); + final boolean isSlippery = (lp.flags & FLAG_SLIPPERY) != 0; + if (isSlippery == slippery) { + return; + } + + if (slippery) { + lp.flags |= FLAG_SLIPPERY; + } else { + lp.flags &= ~FLAG_SLIPPERY; + } + mViewHost.relayout(lp); + } + + private boolean isLandscape() { + return getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE; + } + + private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onDoubleTap(MotionEvent e) { + if (mSplitLayout != null) { + mSplitLayout.onDoubleTappedDivider(); + } + return false; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java new file mode 100644 index 000000000000..60231df37370 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.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.common.split; + +import static android.view.WindowManager.DOCKED_LEFT; +import static android.view.WindowManager.DOCKED_TOP; + +import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END; +import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Rect; +import android.view.SurfaceControl; + +import androidx.annotation.Nullable; + +import com.android.internal.policy.DividerSnapAlgorithm; +import com.android.wm.shell.animation.Interpolators; + +/** + * Records and handles layout of splits. Helps to calculate proper bounds when configuration or + * divide position changes. + */ +public class SplitLayout { + private final int mDividerWindowWidth; + private final int mDividerInsets; + private final int mDividerSize; + + private final Rect mRootBounds = new Rect(); + private final Rect mDividerBounds = new Rect(); + private final Rect mBounds1 = new Rect(); + private final Rect mBounds2 = new Rect(); + private final LayoutChangeListener mLayoutChangeListener; + private final SplitWindowManager mSplitWindowManager; + + private Context mContext; + private DividerSnapAlgorithm mDividerSnapAlgorithm; + private int mDividePosition; + private boolean mInitialized = false; + + public SplitLayout(String windowName, Context context, Configuration configuration, + LayoutChangeListener layoutChangeListener, + SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks) { + mContext = context.createConfigurationContext(configuration); + mLayoutChangeListener = layoutChangeListener; + mSplitWindowManager = new SplitWindowManager( + windowName, mContext, configuration, parentContainerCallbacks); + + mDividerWindowWidth = context.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.docked_stack_divider_thickness); + mDividerInsets = context.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.docked_stack_divider_insets); + mDividerSize = mDividerWindowWidth - mDividerInsets * 2; + + mRootBounds.set(configuration.windowConfiguration.getBounds()); + mDividerSnapAlgorithm = getSnapAlgorithm(context.getResources(), mRootBounds); + resetDividerPosition(); + } + + /** Gets bounds of the primary split. */ + public Rect getBounds1() { + return mBounds1; + } + + /** Gets bounds of the secondary split. */ + public Rect getBounds2() { + return mBounds2; + } + + /** Gets bounds of divider window. */ + public Rect getDividerBounds() { + return mDividerBounds; + } + + /** Returns leash of the current divider bar. */ + @Nullable + public SurfaceControl getDividerLeash() { + return mSplitWindowManager == null ? null : mSplitWindowManager.getSurfaceControl(); + } + + int getDividePosition() { + return mDividePosition; + } + + /** Applies new configuration, returns {@code false} if there's no effect to the layout. */ + public boolean updateConfiguration(Configuration configuration) { + final Rect rootBounds = configuration.windowConfiguration.getBounds(); + if (mRootBounds.equals(rootBounds)) { + return false; + } + + mContext = mContext.createConfigurationContext(configuration); + mSplitWindowManager.setConfiguration(configuration); + mRootBounds.set(rootBounds); + mDividerSnapAlgorithm = getSnapAlgorithm(mContext.getResources(), mRootBounds); + resetDividerPosition(); + + // Don't inflate divider bar if it is not initialized. + if (!mInitialized) { + return false; + } + + release(); + init(); + return true; + } + + /** Updates recording bounds of divider window and both of the splits. */ + private void updateBounds(int position) { + mDividerBounds.set(mRootBounds); + mBounds1.set(mRootBounds); + mBounds2.set(mRootBounds); + if (isLandscape(mRootBounds)) { + position += mRootBounds.left; + mDividerBounds.left = position - mDividerInsets; + mDividerBounds.right = mDividerBounds.left + mDividerWindowWidth; + mBounds1.right = position; + mBounds2.left = mBounds1.right + mDividerSize; + } else { + position += mRootBounds.top; + mDividerBounds.top = position - mDividerInsets; + mDividerBounds.bottom = mDividerBounds.top + mDividerWindowWidth; + mBounds1.bottom = position; + mBounds2.top = mBounds1.bottom + mDividerSize; + } + } + + /** Inflates {@link DividerView} on the root surface. */ + public void init() { + if (mInitialized) return; + mInitialized = true; + mSplitWindowManager.init(this); + } + + /** Releases the surface holding the current {@link DividerView}. */ + public void release() { + if (!mInitialized) return; + mInitialized = false; + mSplitWindowManager.release(); + } + + /** + * Updates bounds with the passing position. Usually used to update recording bounds while + * performing animation or dragging divider bar to resize the splits. + */ + void updateDivideBounds(int position) { + updateBounds(position); + mLayoutChangeListener.onBoundsChanging(this); + mSplitWindowManager.setResizingSplits(true); + } + + void setDividePosition(int position) { + mDividePosition = position; + updateBounds(mDividePosition); + mLayoutChangeListener.onBoundsChanged(this); + mSplitWindowManager.setResizingSplits(false); + } + + /** Resets divider position. */ + public void resetDividerPosition() { + mDividePosition = mDividerSnapAlgorithm.getMiddleTarget().position; + updateBounds(mDividePosition); + } + + /** + * Sets new divide position and updates bounds correspondingly. Notifies listener if the new + * target indicates dismissing split. + */ + public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) { + switch (snapTarget.flag) { + case FLAG_DISMISS_START: + mLayoutChangeListener.onSnappedToDismiss(false /* bottomOrRight */); + mSplitWindowManager.setResizingSplits(false); + break; + case FLAG_DISMISS_END: + mLayoutChangeListener.onSnappedToDismiss(true /* bottomOrRight */); + mSplitWindowManager.setResizingSplits(false); + break; + default: + flingDividePosition(currentPosition, snapTarget.position); + break; + } + } + + void onDoubleTappedDivider() { + mLayoutChangeListener.onDoubleTappedDivider(); + } + + /** + * Returns {@link DividerSnapAlgorithm.SnapTarget} which matches passing position and velocity. + * If hardDismiss is set to {@code true}, it will be harder to reach dismiss target. + */ + public DividerSnapAlgorithm.SnapTarget findSnapTarget(int position, float velocity, + boolean hardDismiss) { + return mDividerSnapAlgorithm.calculateSnapTarget(position, velocity, hardDismiss); + } + + private DividerSnapAlgorithm getSnapAlgorithm(Resources resources, Rect rootBounds) { + final boolean isLandscape = isLandscape(rootBounds); + return new DividerSnapAlgorithm( + resources, + rootBounds.width(), + rootBounds.height(), + mDividerSize, + !isLandscape, + new Rect() /* insets */, + isLandscape ? DOCKED_LEFT : DOCKED_TOP /* dockSide */); + } + + private void flingDividePosition(int from, int to) { + ValueAnimator animator = ValueAnimator + .ofInt(from, to) + .setDuration(250); + animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); + animator.addUpdateListener( + animation -> updateDivideBounds((int) animation.getAnimatedValue())); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + setDividePosition(to); + } + + @Override + public void onAnimationCancel(Animator animation) { + setDividePosition(to); + } + }); + animator.start(); + } + + private static boolean isLandscape(Rect bounds) { + return bounds.width() > bounds.height(); + } + + /** Listens layout change event. */ + public interface LayoutChangeListener { + /** Calls when dismissing split. */ + void onSnappedToDismiss(boolean snappedToEnd); + + /** Calls when the bounds is changing due to animation or dragging divider bar. */ + void onBoundsChanging(SplitLayout layout); + + /** Calls when the target bounds changed. */ + void onBoundsChanged(SplitLayout layout); + + /** Calls when user double tapped on the divider bar. */ + default void onDoubleTappedDivider() { + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java new file mode 100644 index 000000000000..7f9c34f5df7a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.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.common.split; + +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +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.PRIVATE_FLAG_NO_MOVE_ANIMATION; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; + +import android.app.ActivityTaskManager; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.Region; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Slog; +import android.view.IWindow; +import android.view.LayoutInflater; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.WindowManager; +import android.view.WindowlessWindowManager; + +import androidx.annotation.Nullable; + +import com.android.wm.shell.R; + +/** + * Holds view hierarchy of a root surface and helps to inflate {@link DividerView} for a split. + */ +public final class SplitWindowManager extends WindowlessWindowManager { + private static final String TAG = SplitWindowManager.class.getSimpleName(); + + private final ParentContainerCallbacks mParentContainerCallbacks; + private Context mContext; + private SurfaceControlViewHost mViewHost; + private boolean mResizingSplits; + private final String mWindowName; + + public interface ParentContainerCallbacks { + void attachToParentSurface(SurfaceControl.Builder b); + } + + public SplitWindowManager(String windowName, Context context, Configuration config, + ParentContainerCallbacks parentContainerCallbacks) { + super(config, null /* rootSurface */, null /* hostInputToken */); + mContext = context.createConfigurationContext(config); + mParentContainerCallbacks = parentContainerCallbacks; + mWindowName = windowName; + } + + @Override + public void setTouchRegion(IBinder window, Region region) { + super.setTouchRegion(window, region); + } + + @Override + public SurfaceControl getSurfaceControl(IWindow window) { + return super.getSurfaceControl(window); + } + + @Override + public void setConfiguration(Configuration configuration) { + super.setConfiguration(configuration); + mContext = mContext.createConfigurationContext(configuration); + } + + @Override + protected void attachToParentSurface(SurfaceControl.Builder b) { + mParentContainerCallbacks.attachToParentSurface(b); + } + + /** Inflates {@link DividerView} on to the root surface. */ + void init(SplitLayout splitLayout) { + if (mViewHost == null) { + mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this); + } + + final Rect dividerBounds = splitLayout.getDividerBounds(); + final DividerView dividerView = (DividerView) LayoutInflater.from(mContext) + .inflate(R.layout.split_divider, null /* root */); + + WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + dividerBounds.width(), dividerBounds.height(), TYPE_DOCK_DIVIDER, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_WATCH_OUTSIDE_TOUCH + | FLAG_SPLIT_TOUCH | FLAG_SLIPPERY, + PixelFormat.TRANSLUCENT); + lp.token = new Binder(); + lp.setTitle(mWindowName); + lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; + mViewHost.setView(dividerView, lp); + dividerView.setup(splitLayout, mViewHost); + } + + /** + * Releases the surface control of the current {@link DividerView} and tear down the view + * hierarchy. + */ + void release() { + if (mViewHost == null) return; + mViewHost.release(); + mViewHost = null; + } + + void setResizingSplits(boolean resizing) { + if (resizing == mResizingSplits) return; + try { + ActivityTaskManager.getService().setSplitScreenResizing(resizing); + mResizingSplits = resizing; + } catch (RemoteException e) { + Slog.w(TAG, "Error calling setSplitScreenResizing", e); + } + } + + /** + * Gets {@link SurfaceControl} of the surface holding divider view. @return {@code null} if not + * feasible. + */ + @Nullable + SurfaceControl getSurfaceControl() { + return mViewHost == null ? null : getSurfaceControl(mViewHost.getWindowToken()); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java new file mode 100644 index 000000000000..c8938ad40aba --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java @@ -0,0 +1,273 @@ +/* + * 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.draganddrop; + +import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY; +import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT; +import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK; +import static android.view.DragEvent.ACTION_DRAG_ENDED; +import static android.view.DragEvent.ACTION_DRAG_ENTERED; +import static android.view.DragEvent.ACTION_DRAG_EXITED; +import static android.view.DragEvent.ACTION_DRAG_LOCATION; +import static android.view.DragEvent.ACTION_DRAG_STARTED; +import static android.view.DragEvent.ACTION_DROP; +import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; +import static android.view.WindowManager.LayoutParams.MATCH_PARENT; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; +import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + +import android.content.ClipDescription; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.PixelFormat; +import android.util.Slog; +import android.util.SparseArray; +import android.view.DragEvent; +import android.view.LayoutInflater; +import android.view.SurfaceControl; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.splitscreen.SplitScreen; + +import java.util.Optional; + +/** + * Handles the global drag and drop handling for the Shell. + */ +public class DragAndDropController implements DisplayController.OnDisplaysChangedListener, + View.OnDragListener { + + private static final String TAG = DragAndDropController.class.getSimpleName(); + + private final Context mContext; + private final DisplayController mDisplayController; + private SplitScreen mSplitScreen; + + private final SparseArray<PerDisplay> mDisplayDropTargets = new SparseArray<>(); + private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); + + public DragAndDropController(Context context, DisplayController displayController) { + mContext = context; + mDisplayController = displayController; + } + + public void initialize(Optional<SplitScreen> splitscreen) { + mSplitScreen = splitscreen.orElse(null); + mDisplayController.addDisplayWindowListener(this); + } + + @Override + public void onDisplayAdded(int displayId) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Display added: %d", displayId); + final Context context = mDisplayController.getDisplayContext(displayId); + final WindowManager wm = context.getSystemService(WindowManager.class); + + // TODO(b/169894807): Figure out the right layer for this, needs to be below the task bar + final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, + TYPE_APPLICATION_OVERLAY, + FLAG_NOT_FOCUSABLE | FLAG_HARDWARE_ACCELERATED, + PixelFormat.TRANSLUCENT); + layoutParams.privateFlags |= SYSTEM_FLAG_SHOW_FOR_ALL_USERS + | PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP + | PRIVATE_FLAG_NO_MOVE_ANIMATION; + layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + layoutParams.setFitInsetsTypes(0); + layoutParams.setTitle("ShellDropTarget"); + + FrameLayout rootView = (FrameLayout) LayoutInflater.from(context).inflate( + R.layout.global_drop_target, null); + rootView.setOnDragListener(this); + rootView.setVisibility(View.INVISIBLE); + DragLayout dragLayout = new DragLayout(context, mSplitScreen); + rootView.addView(dragLayout, + new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + try { + wm.addView(rootView, layoutParams); + mDisplayDropTargets.put(displayId, + new PerDisplay(displayId, context, wm, rootView, dragLayout)); + } catch (WindowManager.InvalidDisplayException e) { + Slog.w(TAG, "Unable to add view for display id: " + displayId); + } + } + + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Display changed: %d", displayId); + final PerDisplay pd = mDisplayDropTargets.get(displayId); + if (pd == null) { + return; + } + pd.rootView.requestApplyInsets(); + } + + @Override + public void onDisplayRemoved(int displayId) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Display removed: %d", displayId); + final PerDisplay pd = mDisplayDropTargets.get(displayId); + if (pd == null) { + return; + } + pd.wm.removeViewImmediate(pd.rootView); + mDisplayDropTargets.remove(displayId); + } + + @Override + public boolean onDrag(View target, DragEvent event) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Drag event: action=%s x=%f y=%f xOffset=%f yOffset=%f", + DragEvent.actionToString(event.getAction()), event.getX(), event.getY(), + event.getOffsetX(), event.getOffsetY()); + final int displayId = target.getDisplay().getDisplayId(); + final PerDisplay pd = mDisplayDropTargets.get(displayId); + final ClipDescription description = event.getClipDescription(); + + if (pd == null) { + return false; + } + + if (event.getAction() == ACTION_DRAG_STARTED) { + final boolean hasValidClipData = event.getClipData().getItemCount() > 0 + && (description.hasMimeType(MIMETYPE_APPLICATION_ACTIVITY) + || description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT) + || description.hasMimeType(MIMETYPE_APPLICATION_TASK)); + pd.isHandlingDrag = hasValidClipData; + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Clip description: handlingDrag=%b itemCount=%d mimeTypes=%s", + pd.isHandlingDrag, event.getClipData().getItemCount(), + getMimeTypes(description)); + } + + if (!pd.isHandlingDrag) { + return false; + } + + switch (event.getAction()) { + case ACTION_DRAG_STARTED: + if (pd.activeDragCount != 0) { + Slog.w(TAG, "Unexpected drag start during an active drag"); + return false; + } + pd.activeDragCount++; + pd.dragLayout.prepare(mDisplayController.getDisplayLayout(displayId), + event.getClipData()); + setDropTargetWindowVisibility(pd, View.VISIBLE); + break; + case ACTION_DRAG_ENTERED: + pd.dragLayout.show(); + break; + case ACTION_DRAG_LOCATION: + pd.dragLayout.update(event); + break; + case ACTION_DROP: { + return handleDrop(event, pd); + } + case ACTION_DRAG_EXITED: { + // Either one of DROP or EXITED will happen, and when EXITED we won't consume + // the drag surface + pd.dragLayout.hide(event, null); + break; + } + case ACTION_DRAG_ENDED: + // TODO(b/169894807): Ensure sure it's not possible to get ENDED without DROP + // or EXITED + if (!pd.dragLayout.hasDropped()) { + pd.activeDragCount--; + pd.dragLayout.hide(event, () -> { + if (pd.activeDragCount == 0) { + // Hide the window if another drag hasn't been started while animating + // the drag-end + setDropTargetWindowVisibility(pd, View.INVISIBLE); + } + }); + } + break; + } + return true; + } + + /** + * Handles dropping on the drop target. + */ + private boolean handleDrop(DragEvent event, PerDisplay pd) { + final SurfaceControl dragSurface = event.getDragSurface(); + pd.activeDragCount--; + return pd.dragLayout.drop(event, dragSurface, () -> { + if (pd.activeDragCount == 0) { + // Hide the window if another drag hasn't been started while animating the drop + setDropTargetWindowVisibility(pd, View.INVISIBLE); + } + + // Clean up the drag surface + mTransaction.reparent(dragSurface, null); + mTransaction.apply(); + }); + } + + private void setDropTargetWindowVisibility(PerDisplay pd, int visibility) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Set drop target window visibility: displayId=%d visibility=%d", + pd.displayId, visibility); + pd.rootView.setVisibility(visibility); + if (visibility == View.VISIBLE) { + pd.rootView.requestApplyInsets(); + } + } + + private String getMimeTypes(ClipDescription description) { + String mimeTypes = ""; + for (int i = 0; i < description.getMimeTypeCount(); i++) { + if (i > 0) { + mimeTypes += ", "; + } + mimeTypes += description.getMimeType(i); + } + return mimeTypes; + } + + private static class PerDisplay { + final int displayId; + final Context context; + final WindowManager wm; + final FrameLayout rootView; + final DragLayout dragLayout; + + boolean isHandlingDrag; + // A count of the number of active drags in progress to ensure that we only hide the window + // when all the drag animations have completed + int activeDragCount; + + PerDisplay(int dispId, Context c, WindowManager w, FrameLayout rv, DragLayout dl) { + displayId = dispId; + context = c; + wm = w; + rootView = rv; + dragLayout = dl; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java new file mode 100644 index 000000000000..35dcdd5923a8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java @@ -0,0 +1,394 @@ +/* + * 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.draganddrop; + +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.content.ClipDescription.EXTRA_ACTIVITY_OPTIONS; +import static android.content.ClipDescription.EXTRA_PENDING_INTENT; +import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT; +import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK; +import static android.content.Intent.EXTRA_PACKAGE_NAME; +import static android.content.Intent.EXTRA_SHORTCUT_ID; +import static android.content.Intent.EXTRA_TASK_ID; +import static android.content.Intent.EXTRA_USER; + +import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_FULLSCREEN; +import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM; +import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT; +import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT; +import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_TOP; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_UNDEFINED; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; + +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.PendingIntent; +import android.app.WindowConfiguration; +import android.content.ActivityNotFoundException; +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.LauncherApps; +import android.graphics.Insets; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Slog; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.splitscreen.SplitScreen; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; + +/** + * The policy for handling drag and drop operations to shell. + */ +public class DragAndDropPolicy { + + private static final String TAG = DragAndDropPolicy.class.getSimpleName(); + + private final Context mContext; + private final ActivityTaskManager mActivityTaskManager; + private final Starter mStarter; + private final SplitScreen mSplitScreen; + private final ArrayList<DragAndDropPolicy.Target> mTargets = new ArrayList<>(); + + private DragSession mSession; + + public DragAndDropPolicy(Context context, SplitScreen splitScreen) { + this(context, ActivityTaskManager.getInstance(), splitScreen, new DefaultStarter(context)); + } + + @VisibleForTesting + DragAndDropPolicy(Context context, ActivityTaskManager activityTaskManager, + SplitScreen splitScreen, Starter starter) { + mContext = context; + mActivityTaskManager = activityTaskManager; + mSplitScreen = splitScreen; + mStarter = mSplitScreen != null ? mSplitScreen : starter; + } + + /** + * Starts a new drag session with the given initial drag data. + */ + void start(DisplayLayout displayLayout, ClipData data) { + mSession = new DragSession(mContext, mActivityTaskManager, displayLayout, data); + // TODO(b/169894807): Also update the session data with task stack changes + mSession.update(); + } + + /** + * Returns the target's regions based on the current state of the device and display. + */ + @NonNull + ArrayList<Target> getTargets(Insets insets) { + mTargets.clear(); + if (mSession == null) { + // Return early if this isn't an app drag + return mTargets; + } + + final int w = mSession.displayLayout.width(); + final int h = mSession.displayLayout.height(); + final int iw = w - insets.left - insets.right; + final int ih = h - insets.top - insets.bottom; + final int l = insets.left; + final int t = insets.top; + final Rect displayRegion = new Rect(l, t, l + iw, t + ih); + final Rect fullscreenDrawRegion = new Rect(displayRegion); + final Rect fullscreenHitRegion = new Rect(displayRegion); + final boolean inLandscape = mSession.displayLayout.isLandscape(); + final boolean inSplitScreen = mSplitScreen != null && mSplitScreen.isSplitScreenVisible(); + // We allow splitting if we are already in split-screen or the running task is a standard + // task in fullscreen mode. + final boolean allowSplit = inSplitScreen + || (mSession.runningTaskActType == ACTIVITY_TYPE_STANDARD + && mSession.runningTaskWinMode == WINDOWING_MODE_FULLSCREEN); + if (allowSplit) { + // Already split, allow replacing existing split task + final Rect topOrLeftBounds = new Rect(); + final Rect bottomOrRightBounds = new Rect(); + mSplitScreen.getStageBounds(topOrLeftBounds, bottomOrRightBounds); + topOrLeftBounds.intersect(displayRegion); + bottomOrRightBounds.intersect(displayRegion); + + if (inLandscape) { + final Rect leftHitRegion = new Rect(); + final Rect leftDrawRegion = topOrLeftBounds; + final Rect rightHitRegion = new Rect(); + final Rect rightDrawRegion = bottomOrRightBounds; + + displayRegion.splitVertically(leftHitRegion, fullscreenHitRegion, rightHitRegion); + + mTargets.add( + new Target(TYPE_FULLSCREEN, fullscreenHitRegion, fullscreenDrawRegion)); + mTargets.add(new Target(TYPE_SPLIT_LEFT, leftHitRegion, leftDrawRegion)); + mTargets.add(new Target(TYPE_SPLIT_RIGHT, rightHitRegion, rightDrawRegion)); + + } else { + final Rect topHitRegion = new Rect(); + final Rect topDrawRegion = topOrLeftBounds; + final Rect bottomHitRegion = new Rect(); + final Rect bottomDrawRegion = bottomOrRightBounds; + + displayRegion.splitHorizontally( + topHitRegion, fullscreenHitRegion, bottomHitRegion); + + mTargets.add( + new Target(TYPE_FULLSCREEN, fullscreenHitRegion, fullscreenDrawRegion)); + mTargets.add(new Target(TYPE_SPLIT_TOP, topHitRegion, topDrawRegion)); + mTargets.add(new Target(TYPE_SPLIT_BOTTOM, bottomHitRegion, bottomDrawRegion)); + } + } else { + // Split-screen not allowed, so only show the fullscreen target + mTargets.add(new Target(TYPE_FULLSCREEN, fullscreenHitRegion, fullscreenDrawRegion)); + } + return mTargets; + } + + /** + * Returns the target at the given position based on the targets previously calculated. + */ + @Nullable + Target getTargetAtLocation(int x, int y) { + for (int i = mTargets.size() - 1; i >= 0; i--) { + DragAndDropPolicy.Target t = mTargets.get(i); + if (t.hitRegion.contains(x, y)) { + return t; + } + } + return null; + } + + @VisibleForTesting + void handleDrop(Target target, ClipData data) { + if (target == null || !mTargets.contains(target)) { + return; + } + + final boolean inSplitScreen = mSplitScreen != null && mSplitScreen.isSplitScreenVisible(); + final boolean leftOrTop = target.type == TYPE_SPLIT_TOP || target.type == TYPE_SPLIT_LEFT; + + @SplitScreen.StageType int stage = STAGE_TYPE_UNDEFINED; + @SplitScreen.StagePosition int position = STAGE_POSITION_UNDEFINED; + if (target.type != TYPE_FULLSCREEN && mSplitScreen != null) { + // Update launch options for the split side we are targeting. + position = leftOrTop ? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT; + if (!inSplitScreen) { + // Launch in the side stage if we are not in split-screen already. + stage = STAGE_TYPE_SIDE; + } + } + + final ClipDescription description = data.getDescription(); + final Intent dragData = mSession.dragData; + mStarter.startClipDescription(description, dragData, stage, position); + } + + /** + * Per-drag session data. + */ + private static class DragSession { + private final Context mContext; + private final ActivityTaskManager mActivityTaskManager; + private final ClipData mInitialDragData; + + final DisplayLayout displayLayout; + Intent dragData; + int runningTaskId; + @WindowConfiguration.WindowingMode + int runningTaskWinMode = WINDOWING_MODE_UNDEFINED; + @WindowConfiguration.ActivityType + int runningTaskActType = ACTIVITY_TYPE_STANDARD; + boolean runningTaskIsResizeable; + boolean dragItemSupportsSplitscreen; + + DragSession(Context context, ActivityTaskManager activityTaskManager, + DisplayLayout dispLayout, ClipData data) { + mContext = context; + mActivityTaskManager = activityTaskManager; + mInitialDragData = data; + displayLayout = dispLayout; + } + + /** + * Updates the session data based on the current state of the system. + */ + void update() { + + List<ActivityManager.RunningTaskInfo> tasks = + mActivityTaskManager.getTasks(1, false /* filterOnlyVisibleRecents */); + if (!tasks.isEmpty()) { + final ActivityManager.RunningTaskInfo task = tasks.get(0); + runningTaskWinMode = task.getWindowingMode(); + runningTaskActType = task.getActivityType(); + runningTaskId = task.taskId; + runningTaskIsResizeable = task.isResizeable; + } + + final ActivityInfo info = mInitialDragData.getItemAt(0).getActivityInfo(); + dragItemSupportsSplitscreen = info == null + || ActivityInfo.isResizeableMode(info.resizeMode); + dragData = mInitialDragData.getItemAt(0).getIntent(); + } + } + + /** + * Interface for actually committing the task launches. + */ + @VisibleForTesting + public interface Starter { + default void startClipDescription(ClipDescription description, Intent intent, + @SplitScreen.StageType int stage, @SplitScreen.StagePosition int position) { + final boolean isTask = description.hasMimeType(MIMETYPE_APPLICATION_TASK); + final boolean isShortcut = description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT); + final Bundle opts = intent.hasExtra(EXTRA_ACTIVITY_OPTIONS) + ? intent.getBundleExtra(EXTRA_ACTIVITY_OPTIONS) : new Bundle(); + + if (isTask) { + final int taskId = intent.getIntExtra(EXTRA_TASK_ID, INVALID_TASK_ID); + startTask(taskId, stage, position, opts); + } else if (isShortcut) { + final String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); + final String id = intent.getStringExtra(EXTRA_SHORTCUT_ID); + final UserHandle user = intent.getParcelableExtra(EXTRA_USER); + startShortcut(packageName, id, stage, position, opts, user); + } else { + startIntent(intent.getParcelableExtra(EXTRA_PENDING_INTENT), stage, position, opts); + } + } + void startTask(int taskId, @SplitScreen.StageType int stage, + @SplitScreen.StagePosition int position, @Nullable Bundle options); + void startShortcut(String packageName, String shortcutId, + @SplitScreen.StageType int stage, @SplitScreen.StagePosition int position, + @Nullable Bundle options, UserHandle user); + void startIntent(PendingIntent intent, @SplitScreen.StageType int stage, + @SplitScreen.StagePosition int position, @Nullable Bundle options); + void enterSplitScreen(int taskId, boolean leftOrTop); + void exitSplitScreen(); + } + + /** + * Default implementation of the starter which calls through the system services to launch the + * tasks. + */ + private static class DefaultStarter implements Starter { + private final Context mContext; + + public DefaultStarter(Context context) { + mContext = context; + } + + @Override + public void startTask(int taskId, int stage, int position, + @Nullable Bundle options) { + try { + ActivityTaskManager.getService().startActivityFromRecents(taskId, options); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to launch task", e); + } + } + + @Override + public void startShortcut(String packageName, String shortcutId, int stage, int position, + @Nullable Bundle options, UserHandle user) { + try { + LauncherApps launcherApps = + mContext.getSystemService(LauncherApps.class); + launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */, + options, user); + } catch (ActivityNotFoundException e) { + Slog.e(TAG, "Failed to launch shortcut", e); + } + } + + @Override + public void startIntent(PendingIntent intent, int stage, int position, + @Nullable Bundle options) { + try { + intent.send(null, 0, null, null, null, null, options); + } catch (PendingIntent.CanceledException e) { + Slog.e(TAG, "Failed to launch activity", e); + } + } + + @Override + public void enterSplitScreen(int taskId, boolean leftOrTop) { + throw new UnsupportedOperationException("enterSplitScreen not implemented by starter"); + } + + @Override + public void exitSplitScreen() { + throw new UnsupportedOperationException("exitSplitScreen not implemented by starter"); + } + } + + /** + * Represents a drop target. + */ + static class Target { + static final int TYPE_FULLSCREEN = 0; + static final int TYPE_SPLIT_LEFT = 1; + static final int TYPE_SPLIT_TOP = 2; + static final int TYPE_SPLIT_RIGHT = 3; + static final int TYPE_SPLIT_BOTTOM = 4; + @IntDef(value = { + TYPE_FULLSCREEN, + TYPE_SPLIT_LEFT, + TYPE_SPLIT_TOP, + TYPE_SPLIT_RIGHT, + TYPE_SPLIT_BOTTOM + }) + @Retention(RetentionPolicy.SOURCE) + @interface Type{} + + final @Type int type; + + // The actual hit region for this region + final Rect hitRegion; + // The approximate visual region for where the task will start + final Rect drawRegion; + + public Target(@Type int t, Rect hit, Rect draw) { + type = t; + hitRegion = hit; + drawRegion = draw; + } + + @Override + public String toString() { + return "Target {hit=" + hitRegion + " draw=" + drawRegion + "}"; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java new file mode 100644 index 000000000000..82c4e440fb15 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java @@ -0,0 +1,202 @@ +/* + * 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.draganddrop; + +import static com.android.wm.shell.animation.Interpolators.FAST_OUT_LINEAR_IN; +import static com.android.wm.shell.animation.Interpolators.FAST_OUT_SLOW_IN; +import static com.android.wm.shell.animation.Interpolators.LINEAR; +import static com.android.wm.shell.animation.Interpolators.LINEAR_OUT_SLOW_IN; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.content.ClipData; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Insets; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.view.DragEvent; +import android.view.SurfaceControl; +import android.view.View; +import android.view.WindowInsets; +import android.view.WindowInsets.Type; + +import androidx.annotation.NonNull; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.splitscreen.SplitScreen; + +import java.util.ArrayList; + +/** + * Coordinates the visible drop targets for the current drag. + */ +public class DragLayout extends View { + + private final DragAndDropPolicy mPolicy; + + private DragAndDropPolicy.Target mCurrentTarget = null; + private DropOutlineDrawable mDropOutline; + private int mDisplayMargin; + private Insets mInsets = Insets.NONE; + + private boolean mIsShowing; + private boolean mHasDropped; + + public DragLayout(Context context, SplitScreen splitscreen) { + super(context); + mPolicy = new DragAndDropPolicy(context, splitscreen); + mDisplayMargin = context.getResources().getDimensionPixelSize( + R.dimen.drop_layout_display_margin); + mDropOutline = new DropOutlineDrawable(context); + setBackground(mDropOutline); + setWillNotDraw(false); + } + + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + mInsets = insets.getInsets(Type.systemBars() | Type.displayCutout()); + recomputeDropTargets(); + return super.onApplyWindowInsets(insets); + } + + @Override + protected boolean verifyDrawable(@NonNull Drawable who) { + return who == mDropOutline || super.verifyDrawable(who); + } + + @Override + protected void onDraw(Canvas canvas) { + if (mCurrentTarget != null) { + mDropOutline.draw(canvas); + } + } + + public boolean hasDropTarget() { + return mCurrentTarget != null; + } + + public boolean hasDropped() { + return mHasDropped; + } + + public void prepare(DisplayLayout displayLayout, ClipData initialData) { + mPolicy.start(displayLayout, initialData); + mHasDropped = false; + mCurrentTarget = null; + } + + public void show() { + mIsShowing = true; + recomputeDropTargets(); + } + + /** + * Recalculates the drop targets based on the current policy. + */ + private void recomputeDropTargets() { + if (!mIsShowing) { + return; + } + final ArrayList<DragAndDropPolicy.Target> targets = mPolicy.getTargets(mInsets); + for (int i = 0; i < targets.size(); i++) { + final DragAndDropPolicy.Target target = targets.get(i); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Add target: %s", target); + // Inset the draw region by a little bit + target.drawRegion.inset(mDisplayMargin, mDisplayMargin); + } + } + + /** + * Updates the visible drop target as the user drags. + */ + public void update(DragEvent event) { + // Find containing region, if the same as mCurrentRegion, then skip, otherwise, animate the + // visibility of the current region + DragAndDropPolicy.Target target = mPolicy.getTargetAtLocation( + (int) event.getX(), (int) event.getY()); + if (mCurrentTarget != target) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Current target: %s", target); + if (target == null) { + // Animating to no target + mDropOutline.startVisibilityAnimation(false, LINEAR); + Rect finalBounds = new Rect(mCurrentTarget.drawRegion); + finalBounds.inset(mDisplayMargin, mDisplayMargin); + mDropOutline.startBoundsAnimation(finalBounds, FAST_OUT_LINEAR_IN); + } else if (mCurrentTarget == null) { + // Animating to first target + mDropOutline.startVisibilityAnimation(true, LINEAR); + Rect initialBounds = new Rect(target.drawRegion); + initialBounds.inset(mDisplayMargin, mDisplayMargin); + mDropOutline.setRegionBounds(initialBounds); + mDropOutline.startBoundsAnimation(target.drawRegion, LINEAR_OUT_SLOW_IN); + } else { + // Bounds change + mDropOutline.startBoundsAnimation(target.drawRegion, FAST_OUT_SLOW_IN); + } + mCurrentTarget = target; + } + } + + /** + * Hides the drag layout and animates out the visible drop targets. + */ + public void hide(DragEvent event, Runnable hideCompleteCallback) { + mIsShowing = false; + ObjectAnimator alphaAnimator = mDropOutline.startVisibilityAnimation(false, LINEAR); + ObjectAnimator boundsAnimator = null; + if (mCurrentTarget != null) { + Rect finalBounds = new Rect(mCurrentTarget.drawRegion); + finalBounds.inset(mDisplayMargin, mDisplayMargin); + boundsAnimator = mDropOutline.startBoundsAnimation(finalBounds, FAST_OUT_LINEAR_IN); + } + + if (hideCompleteCallback != null) { + ObjectAnimator lastAnim = boundsAnimator != null + ? boundsAnimator + : alphaAnimator; + lastAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + hideCompleteCallback.run(); + } + }); + } + + mCurrentTarget = null; + } + + /** + * Handles the drop onto a target and animates out the visible drop targets. + */ + public boolean drop(DragEvent event, SurfaceControl dragSurface, + Runnable dropCompleteCallback) { + final boolean handledDrop = mCurrentTarget != null; + mHasDropped = true; + + // Process the drop + mPolicy.handleDrop(mCurrentTarget, event.getClipData()); + + // TODO(b/169894807): Coordinate with dragSurface + hide(event, dropCompleteCallback); + return handledDrop; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropOutlineDrawable.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropOutlineDrawable.java new file mode 100644 index 000000000000..64f7be5be813 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropOutlineDrawable.java @@ -0,0 +1,165 @@ +/* + * 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.draganddrop; + +import android.animation.ObjectAnimator; +import android.animation.RectEvaluator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.IntProperty; +import android.util.Property; +import android.view.animation.Interpolator; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.graphics.ColorUtils; +import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.R; + +/** + * Drawable to draw the region that the target will have once it is dropped. + */ +public class DropOutlineDrawable extends Drawable { + + private static final int BOUNDS_DURATION = 200; + private static final int ALPHA_DURATION = 135; + + private final IntProperty<DropOutlineDrawable> ALPHA = + new IntProperty<DropOutlineDrawable>("alpha") { + @Override + public void setValue(DropOutlineDrawable d, int alpha) { + d.setAlpha(alpha); + } + + @Override + public Integer get(DropOutlineDrawable d) { + return d.getAlpha(); + } + }; + + private final Property<DropOutlineDrawable, Rect> BOUNDS = + new Property<DropOutlineDrawable, Rect>(Rect.class, "bounds") { + @Override + public void set(DropOutlineDrawable d, Rect bounds) { + d.setRegionBounds(bounds); + } + + @Override + public Rect get(DropOutlineDrawable d) { + return d.getRegionBounds(); + } + }; + + private final RectEvaluator mRectEvaluator = new RectEvaluator(new Rect()); + private ObjectAnimator mBoundsAnimator; + private ObjectAnimator mAlphaAnimator; + + private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Rect mBounds = new Rect(); + private final float mCornerRadius; + private final int mMaxAlpha; + private int mColor; + + public DropOutlineDrawable(Context context) { + super(); + // TODO(b/169894807): Use corner specific radii and maybe lower radius for non-edge corners + mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context.getResources()); + mColor = context.getColor(R.color.drop_outline_background); + mMaxAlpha = Color.alpha(mColor); + // Initialize as hidden + ALPHA.set(this, 0); + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + // Do nothing + } + + @Override + public void setAlpha(int alpha) { + mColor = ColorUtils.setAlphaComponent(mColor, alpha); + mPaint.setColor(mColor); + invalidateSelf(); + } + + @Override + public int getAlpha() { + return Color.alpha(mColor); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + protected void onBoundsChange(Rect bounds) { + invalidateSelf(); + } + + @Override + public void draw(@NonNull Canvas canvas) { + canvas.drawRoundRect(mBounds.left, mBounds.top, mBounds.right, mBounds.bottom, + mCornerRadius, mCornerRadius, mPaint); + } + + public void setRegionBounds(Rect bounds) { + mBounds.set(bounds); + invalidateSelf(); + } + + public Rect getRegionBounds() { + return mBounds; + } + + ObjectAnimator startBoundsAnimation(Rect toBounds, Interpolator interpolator) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Animate bounds: from=%s to=%s", + mBounds, toBounds); + if (mBoundsAnimator != null) { + mBoundsAnimator.cancel(); + } + mBoundsAnimator = ObjectAnimator.ofObject(this, BOUNDS, mRectEvaluator, + mBounds, toBounds); + mBoundsAnimator.setDuration(BOUNDS_DURATION); + mBoundsAnimator.setInterpolator(interpolator); + mBoundsAnimator.start(); + return mBoundsAnimator; + } + + ObjectAnimator startVisibilityAnimation(boolean visible, Interpolator interpolator) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Animate alpha: from=%d to=%d", + Color.alpha(mColor), visible ? mMaxAlpha : 0); + if (mAlphaAnimator != null) { + mAlphaAnimator.cancel(); + } + mAlphaAnimator = ObjectAnimator.ofInt(this, ALPHA, Color.alpha(mColor), + visible ? mMaxAlpha : 0); + mAlphaAnimator.setDuration(ALPHA_DURATION); + mAlphaAnimator.setInterpolator(interpolator); + mAlphaAnimator.start(); + return mAlphaAnimator; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutout.java new file mode 100644 index 000000000000..3a2f0da6bf03 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutout.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.hidedisplaycutout; + +import android.content.res.Configuration; + +import androidx.annotation.NonNull; + +import com.android.wm.shell.common.annotations.ExternalThread; + +import java.io.PrintWriter; + +/** + * Interface to engage hide display cutout feature. + */ +@ExternalThread +public interface HideDisplayCutout { + /** + * Notifies {@link Configuration} changed. + * @param newConfig + */ + void onConfigurationChanged(Configuration newConfig); + + /** + * Dumps hide display cutout status. + */ + void dump(@NonNull PrintWriter pw); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java new file mode 100644 index 000000000000..12b8b87f1285 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java @@ -0,0 +1,124 @@ +/* + * 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.hidedisplaycutout; + +import android.content.Context; +import android.content.res.Configuration; +import android.os.SystemProperties; +import android.util.Slog; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; + +import java.io.PrintWriter; +import java.util.concurrent.TimeUnit; + +/** + * Manages the hide display cutout status. + */ +public class HideDisplayCutoutController { + private static final String TAG = "HideDisplayCutoutController"; + + private final Context mContext; + private final HideDisplayCutoutOrganizer mOrganizer; + private final ShellExecutor mMainExecutor; + private final HideDisplayCutoutImpl mImpl = new HideDisplayCutoutImpl(); + @VisibleForTesting + boolean mEnabled; + + HideDisplayCutoutController(Context context, HideDisplayCutoutOrganizer organizer, + ShellExecutor mainExecutor) { + mContext = context; + mOrganizer = organizer; + mMainExecutor = mainExecutor; + updateStatus(); + } + + /** + * Creates {@link HideDisplayCutoutController}, returns {@code null} if the feature is not + * supported. + */ + @Nullable + public static HideDisplayCutout create( + Context context, DisplayController displayController, ShellExecutor mainExecutor) { + // The SystemProperty is set for devices that support this feature and is used to control + // whether to create the HideDisplayCutout instance. + // It's defined in the device.mk (e.g. device/google/crosshatch/device.mk). + if (!SystemProperties.getBoolean("ro.support_hide_display_cutout", false)) { + return null; + } + + HideDisplayCutoutOrganizer organizer = + new HideDisplayCutoutOrganizer(context, displayController, mainExecutor); + return new HideDisplayCutoutController(context, organizer, mainExecutor).mImpl; + } + + @VisibleForTesting + void updateStatus() { + // The config value is used for controlling enabling/disabling status of the feature and is + // defined in the config.xml in a "Hide Display Cutout" overlay package (e.g. device/google/ + // crosshatch/crosshatch/overlay/packages/apps/overlays/NoCutoutOverlay). + final boolean enabled = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_hideDisplayCutoutWithDisplayArea); + if (enabled == mEnabled) { + return; + } + + mEnabled = enabled; + if (enabled) { + mOrganizer.enableHideDisplayCutout(); + } else { + mOrganizer.disableHideDisplayCutout(); + } + } + + private void onConfigurationChanged(Configuration newConfig) { + updateStatus(); + } + + private void dump(@NonNull PrintWriter pw) { + final String prefix = " "; + pw.print(TAG); + pw.println(" states: "); + pw.print(prefix); + pw.print("mEnabled="); + pw.println(mEnabled); + mOrganizer.dump(pw); + } + + private class HideDisplayCutoutImpl implements HideDisplayCutout { + @Override + public void onConfigurationChanged(Configuration newConfig) { + mMainExecutor.execute(() -> { + HideDisplayCutoutController.this.onConfigurationChanged(newConfig); + }); + } + + @Override + public void dump(@NonNull PrintWriter pw) { + try { + mMainExecutor.executeBlocking(() -> HideDisplayCutoutController.this.dump(pw)); + } catch (InterruptedException e) { + Slog.e(TAG, "Failed to dump HideDisplayCutoutController in 2s"); + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java new file mode 100644 index 000000000000..53dd391a01af --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java @@ -0,0 +1,335 @@ +/* + * 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.hidedisplaycutout; + +import static android.view.Display.DEFAULT_DISPLAY; + +import android.content.Context; +import android.graphics.Insets; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.ArrayMap; +import android.util.Log; +import android.util.RotationUtils; +import android.view.Display; +import android.view.DisplayCutout; +import android.view.Surface; +import android.view.SurfaceControl; +import android.window.DisplayAreaAppearedInfo; +import android.window.DisplayAreaInfo; +import android.window.DisplayAreaOrganizer; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.android.internal.R; +import com.android.wm.shell.common.DisplayChangeController; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; + +import java.io.PrintWriter; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Manages the display areas of hide display cutout feature. + */ +class HideDisplayCutoutOrganizer extends DisplayAreaOrganizer { + private static final String TAG = "HideDisplayCutoutOrganizer"; + + private final Context mContext; + private final DisplayController mDisplayController; + + @VisibleForTesting + @GuardedBy("this") + ArrayMap<WindowContainerToken, SurfaceControl> mDisplayAreaMap = new ArrayMap(); + // The default display bound in natural orientation. + private final Rect mDefaultDisplayBounds = new Rect(); + @VisibleForTesting + final Rect mCurrentDisplayBounds = new Rect(); + // The default display cutout in natural orientation. + private Insets mDefaultCutoutInsets; + private Insets mCurrentCutoutInsets; + private boolean mIsDefaultPortrait; + private int mStatusBarHeight; + @VisibleForTesting + int mOffsetX; + @VisibleForTesting + int mOffsetY; + @VisibleForTesting + int mRotation; + + /** + * Handles rotation based on OnDisplayChangingListener callback. + */ + private final DisplayChangeController.OnDisplayChangingListener mRotationController = + (display, fromRotation, toRotation, wct) -> { + mRotation = toRotation; + updateBoundsAndOffsets(true /* enable */); + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + applyAllBoundsAndOffsets(wct, t); + // Only apply t here since the server will do the wct.apply when the method + // finishes. + t.apply(); + }; + + HideDisplayCutoutOrganizer(Context context, DisplayController displayController, + ShellExecutor mainExecutor) { + super(mainExecutor); + mContext = context; + mDisplayController = displayController; + } + + @Override + public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo, + @NonNull SurfaceControl leash) { + if (!addDisplayAreaInfoAndLeashToMap(displayAreaInfo, leash)) { + return; + } + final WindowContainerTransaction wct = new WindowContainerTransaction(); + final SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + applyBoundsAndOffsets(displayAreaInfo.token, leash, wct, tx); + applyTransaction(wct, tx); + } + + @Override + public void onDisplayAreaVanished(@NonNull DisplayAreaInfo displayAreaInfo) { + synchronized (this) { + if (!mDisplayAreaMap.containsKey(displayAreaInfo.token)) { + Log.w(TAG, "Unrecognized token: " + displayAreaInfo.token); + return; + } + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + applyBoundsAndOffsets( + displayAreaInfo.token, mDisplayAreaMap.get(displayAreaInfo.token), wct, t); + applyTransaction(wct, t); + mDisplayAreaMap.remove(displayAreaInfo.token); + } + } + + private void updateDisplayAreaMap(List<DisplayAreaAppearedInfo> displayAreaInfos) { + for (int i = 0; i < displayAreaInfos.size(); i++) { + final DisplayAreaInfo info = displayAreaInfos.get(i).getDisplayAreaInfo(); + final SurfaceControl leash = displayAreaInfos.get(i).getLeash(); + addDisplayAreaInfoAndLeashToMap(info, leash); + } + } + + @VisibleForTesting + boolean addDisplayAreaInfoAndLeashToMap(@NonNull DisplayAreaInfo displayAreaInfo, + @NonNull SurfaceControl leash) { + synchronized (this) { + if (displayAreaInfo.displayId != DEFAULT_DISPLAY) { + return false; + } + if (mDisplayAreaMap.containsKey(displayAreaInfo.token)) { + Log.w(TAG, "Already appeared token: " + displayAreaInfo.token); + return false; + } + mDisplayAreaMap.put(displayAreaInfo.token, leash); + return true; + } + } + + /** + * Enables hide display cutout. + */ + void enableHideDisplayCutout() { + mDisplayController.addDisplayChangingController(mRotationController); + final Display display = mDisplayController.getDisplay(DEFAULT_DISPLAY); + if (display != null) { + mRotation = display.getRotation(); + } + final List<DisplayAreaAppearedInfo> displayAreaInfos = + registerOrganizer(DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT); + updateDisplayAreaMap(displayAreaInfos); + updateBoundsAndOffsets(true /* enabled */); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + applyAllBoundsAndOffsets(wct, t); + applyTransaction(wct, t); + } + + /** + * Disables hide display cutout. + */ + void disableHideDisplayCutout() { + updateBoundsAndOffsets(false /* enabled */); + mDisplayController.removeDisplayChangingController(mRotationController); + unregisterOrganizer(); + } + + @VisibleForTesting + Insets getDisplayCutoutInsetsOfNaturalOrientation() { + final Display display = mDisplayController.getDisplay(DEFAULT_DISPLAY); + if (display == null) { + return Insets.NONE; + } + DisplayCutout cutout = display.getCutout(); + Insets insets = cutout != null ? Insets.of(cutout.getSafeInsets()) : Insets.NONE; + return mRotation != Surface.ROTATION_0 + ? RotationUtils.rotateInsets(insets, 4 /* total number of rotation */ - mRotation) + : insets; + } + + @VisibleForTesting + Rect getDisplayBoundsOfNaturalOrientation() { + Point realSize = new Point(0, 0); + final Display display = mDisplayController.getDisplay(DEFAULT_DISPLAY); + if (display != null) { + display.getRealSize(realSize); + } + final boolean isDisplaySizeFlipped = isDisplaySizeFlipped(); + return new Rect( + 0, + 0, + isDisplaySizeFlipped ? realSize.y : realSize.x, + isDisplaySizeFlipped ? realSize.x : realSize.y); + } + + private boolean isDisplaySizeFlipped() { + return mRotation == Surface.ROTATION_90 || mRotation == Surface.ROTATION_270; + } + + /** + * Updates bounds and offsets according to current state. + * + * @param enabled whether the hide display cutout feature is enabled. + */ + @VisibleForTesting + void updateBoundsAndOffsets(boolean enabled) { + if (!enabled) { + resetBoundsAndOffsets(); + } else { + initDefaultValuesIfNeeded(); + + // Reset to default values. + mCurrentDisplayBounds.set(mDefaultDisplayBounds); + mOffsetX = 0; + mOffsetY = 0; + + // Update bounds and insets according to the rotation. + mCurrentCutoutInsets = RotationUtils.rotateInsets(mDefaultCutoutInsets, mRotation); + if (isDisplaySizeFlipped()) { + mCurrentDisplayBounds.set( + mCurrentDisplayBounds.top, + mCurrentDisplayBounds.left, + mCurrentDisplayBounds.bottom, + mCurrentDisplayBounds.right); + } + mCurrentDisplayBounds.inset(mCurrentCutoutInsets); + + // Replace the top bound with the max(status bar height, cutout height) if there is + // cutout on the top side. + mStatusBarHeight = getStatusBarHeight(); + if (mCurrentCutoutInsets.top != 0) { + mCurrentDisplayBounds.top = Math.max(mStatusBarHeight, mCurrentCutoutInsets.top); + } + mOffsetX = mCurrentDisplayBounds.left; + mOffsetY = mCurrentDisplayBounds.top; + } + } + + private void resetBoundsAndOffsets() { + mCurrentDisplayBounds.setEmpty(); + mOffsetX = 0; + mOffsetY = 0; + } + + private void initDefaultValuesIfNeeded() { + if (!mDefaultDisplayBounds.isEmpty()) { + return; + } + mDefaultDisplayBounds.set(getDisplayBoundsOfNaturalOrientation()); + mDefaultCutoutInsets = getDisplayCutoutInsetsOfNaturalOrientation(); + mIsDefaultPortrait = mDefaultDisplayBounds.width() < mDefaultDisplayBounds.height(); + } + + private void applyAllBoundsAndOffsets( + WindowContainerTransaction wct, SurfaceControl.Transaction t) { + synchronized (this) { + mDisplayAreaMap.forEach((token, leash) -> { + applyBoundsAndOffsets(token, leash, wct, t); + }); + } + } + + @VisibleForTesting + void applyBoundsAndOffsets(WindowContainerToken token, SurfaceControl leash, + WindowContainerTransaction wct, SurfaceControl.Transaction t) { + wct.setBounds(token, mCurrentDisplayBounds); + t.setPosition(leash, mOffsetX, mOffsetY); + t.setWindowCrop(leash, mCurrentDisplayBounds.width(), mCurrentDisplayBounds.height()); + } + + @VisibleForTesting + void applyTransaction(WindowContainerTransaction wct, SurfaceControl.Transaction t) { + applyTransaction(wct); + t.apply(); + } + + private int getStatusBarHeight() { + final boolean isLandscape = + mIsDefaultPortrait ? isDisplaySizeFlipped() : !isDisplaySizeFlipped(); + return mContext.getResources().getDimensionPixelSize( + isLandscape ? R.dimen.status_bar_height_landscape + : R.dimen.status_bar_height_portrait); + } + + void dump(@NonNull PrintWriter pw) { + final String prefix = " "; + pw.print(TAG); + pw.println(" states: "); + synchronized (this) { + pw.print(prefix); + pw.print("mDisplayAreaMap="); + pw.println(mDisplayAreaMap); + } + pw.print(prefix); + pw.print("getDisplayBoundsOfNaturalOrientation()="); + pw.println(getDisplayBoundsOfNaturalOrientation()); + pw.print(prefix); + pw.print("mDefaultDisplayBounds="); + pw.println(mDefaultDisplayBounds); + pw.print(prefix); + pw.print("mCurrentDisplayBounds="); + pw.println(mCurrentDisplayBounds); + pw.print(prefix); + pw.print("mDefaultCutoutInsets="); + pw.println(mDefaultCutoutInsets); + pw.print(prefix); + pw.print("mCurrentCutoutInsets="); + pw.println(mCurrentCutoutInsets); + pw.print(prefix); + pw.print("mRotation="); + pw.println(mRotation); + pw.print(prefix); + pw.print("mStatusBarHeight="); + pw.println(mStatusBarHeight); + pw.print(prefix); + pw.print("mOffsetX="); + pw.println(mOffsetX); + pw.print(prefix); + pw.print("mOffsetY="); + pw.println(mOffsetY); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerImeController.java new file mode 100644 index 000000000000..7ce9014fc9ba --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerImeController.java @@ -0,0 +1,417 @@ +/* + * 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.legacysplitscreen; + +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.util.Slog; +import android.view.Choreographer; +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.ShellExecutor; +import com.android.wm.shell.common.TransactionPool; + +class DividerImeController implements DisplayImeController.ImePositionProcessor { + private static final String TAG = "DividerImeController"; + private static final boolean DEBUG = LegacySplitScreenController.DEBUG; + + private static final float ADJUSTED_NONFOCUS_DIM = 0.3f; + + private final LegacySplitScreenTaskListener mSplits; + private final TransactionPool mTransactionPool; + private final ShellExecutor mMainExecutor; + 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(LegacySplitScreenTaskListener splits, TransactionPool pool, + ShellExecutor mainExecutor, TaskOrganizer taskOrganizer) { + mSplits = splits; + mTransactionPool = pool; + mMainExecutor = mainExecutor; + mTaskOrganizer = taskOrganizer; + } + + private DividerView getView() { + return mSplits.mSplitScreenController.getDividerView(); + } + + private LegacySplitDisplayLayout 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 LegacySplitDisplayLayout 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 LegacySplitDisplayLayout 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.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId()); + 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()); + mMainExecutor.execute(() -> { + 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()); + mMainExecutor.execute(() -> { + if (DEBUG) Slog.d(TAG, "ime resume run posted " + dumpState()); + if (!mPaused) { + return; + } + mPaused = false; + mTargetAdjusted = mPausedTargetAdjusted; + updateDimTargets(); + final DividerView view = getView(); + if ((mTargetAdjusted != mAdjusted) && !mSplits.mSplitScreenController.isMinimized() + && view != null) { + // End unminimize animations since they conflict with adjustment animations. + view.finishAnimations(); + } + updateImeAdjustState(); + startAsyncAnimation(); + }); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerState.java new file mode 100644 index 000000000000..af2ab158ab46 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerState.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.legacysplitscreen; + +/** + * Class to hold state of divider that needs to persist across configuration changes. + */ +final class DividerState { + public boolean animateAfterRecentsDrawn; + public float mRatioPositionBeforeMinimized; +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java new file mode 100644 index 000000000000..e13a1dbe22d6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java @@ -0,0 +1,1342 @@ +/* + * 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.legacysplitscreen; + +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 static com.android.wm.shell.common.split.DividerView.TOUCH_ANIMATION_DURATION; +import static com.android.wm.shell.common.split.DividerView.TOUCH_RELEASE_ANIMATION_DURATION; + +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.RemoteException; +import android.util.AttributeSet; +import android.util.Slog; +import android.view.Choreographer; +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.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 com.android.wm.shell.common.split.DividerHandleView; + +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 = LegacySplitScreenController.DEBUG; + + interface DividerCallbacks { + void onDraggingStart(); + void onDraggingEnd(); + } + + 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 LegacySplitScreenController mSplitScreenController; + private WindowManagerProxy mWindowManagerProxy; + private DividerWindowManager mWindowManager; + private VelocityTracker mVelocityTracker; + private FlingAnimationUtils mFlingAnimationUtils; + private LegacySplitDisplayLayout mSplitLayout; + private DividerImeController mImeController; + private DividerCallbacks mCallback; + + private AnimationHandler mSfVsyncAnimationHandler; + 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 LegacySplitScreenTaskListener 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 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); + } + + public void setAnimationHandler(AnimationHandler sfVsyncAnimationHandler) { + mSfVsyncAnimationHandler = sfVsyncAnimationHandler; + } + + @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(LegacySplitScreenController splitScreenController, + DividerWindowManager windowManager, DividerState dividerState, + DividerCallbacks callback, LegacySplitScreenTaskListener tiles, + LegacySplitDisplayLayout 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"); + getHandler().postDelayed(() -> endAction.accept(cancelled), delay); + } + } + }); + mCurrentAnimator = anim; + mCurrentAnimator.setAnimationHandler(mSfVsyncAnimationHandler); + 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) { + getHandler().removeCallbacks(mUpdateEmbeddedMatrix); + getHandler().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.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId()); + 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/legacysplitscreen/DividerWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerWindowManager.java new file mode 100644 index 000000000000..2c3ae68e4749 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerWindowManager.java @@ -0,0 +1,117 @@ +/* + * 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.legacysplitscreen; + +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.PRIVATE_FLAG_TRUSTED_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; +import static android.view.WindowManager.SHELL_ROOT_LAYER_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 | PRIVATE_FLAG_TRUSTED_OVERLAY; + 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, SHELL_ROOT_LAYER_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/legacysplitscreen/ForcedResizableInfoActivity.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/ForcedResizableInfoActivity.java new file mode 100644 index 000000000000..4fe28e630114 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/ForcedResizableInfoActivity.java @@ -0,0 +1,110 @@ +/* + * 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.legacysplitscreen; + +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. + * + * Note: This activity runs on the main thread of the process hosting the Shell lib. + */ +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/legacysplitscreen/ForcedResizableInfoActivityController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/ForcedResizableInfoActivityController.java new file mode 100644 index 000000000000..139544f951ce --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/ForcedResizableInfoActivityController.java @@ -0,0 +1,150 @@ +/* + * 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.legacysplitscreen; + + +import static com.android.wm.shell.legacysplitscreen.ForcedResizableInfoActivity.EXTRA_FORCED_RESIZEABLE_REASON; + +import android.app.ActivityOptions; +import android.content.Context; +import android.content.Intent; +import android.os.UserHandle; +import android.util.ArraySet; +import android.widget.Toast; + +import com.android.wm.shell.R; +import com.android.wm.shell.common.ShellExecutor; + +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 ShellExecutor mMainExecutor; + 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, + LegacySplitScreenController splitScreenController, + ShellExecutor mainExecutor) { + mContext = context; + mMainExecutor = mainExecutor; + splitScreenController.registerInSplitScreenListener(mDockedStackExistsListener); + } + + @Override + public void onDraggingStart() { + mDividerDragging = true; + mMainExecutor.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() { + mMainExecutor.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() { + mMainExecutor.removeCallbacks(mTimeoutRunnable); + mMainExecutor.executeDelayed(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/legacysplitscreen/LegacySplitDisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java new file mode 100644 index 000000000000..477ec339f1db --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java @@ -0,0 +1,314 @@ +/* + * 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.legacysplitscreen; + +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 LegacySplitDisplayLayout { + /** 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; + + LegacySplitScreenTaskListener 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 LegacySplitDisplayLayout(Context ctx, DisplayLayout dl, + LegacySplitScreenTaskListener 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/legacysplitscreen/LegacySplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreen.java new file mode 100644 index 000000000000..499a9c5fa631 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreen.java @@ -0,0 +1,85 @@ +/* + * 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.legacysplitscreen; + +import android.graphics.Rect; +import android.window.WindowContainerToken; + +import com.android.wm.shell.common.annotations.ExternalThread; + +import java.io.PrintWriter; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Interface to engage split screen feature. + */ +@ExternalThread +public interface LegacySplitScreen { + /** 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 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); + + /** Unregisters listener that gets called whenever the existence of the divider changes. */ + void unregisterInSplitScreenListener(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(); + + /** + * Exits the split to make the primary task fullscreen. + */ + void dismissSplitToPrimaryTask(); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java new file mode 100644 index 000000000000..bca6deb451c9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java @@ -0,0 +1,764 @@ +/* + * 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.legacysplitscreen; + +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.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.view.Display.DEFAULT_DISPLAY; + +import android.animation.AnimationHandler; +import android.app.ActivityManager; +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.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 androidx.annotation.Nullable; + +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.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.common.TaskStackListenerCallback; +import com.android.wm.shell.common.TaskStackListenerImpl; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.transition.Transitions; + +import java.io.PrintWriter; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Controls split screen feature. + */ +public class LegacySplitScreenController implements 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 ShellExecutor mMainExecutor; + private final AnimationHandler mSfVsyncAnimationHandler; + private final LegacySplitScreenTaskListener mSplits; + private final SystemWindows mSystemWindows; + final TransactionPool mTransactionPool; + private final WindowManagerProxy mWindowManagerProxy; + private final TaskOrganizer mTaskOrganizer; + private final SplitScreenImpl mImpl = new SplitScreenImpl(); + + private final CopyOnWriteArrayList<WeakReference<Consumer<Boolean>>> mDockedStackExistsListeners + = new CopyOnWriteArrayList<>(); + 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 LegacySplitDisplayLayout 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 LegacySplitDisplayLayout mRotateSplitLayout; + + private boolean mIsKeyguardShowing; + private boolean mVisible = false; + private volatile boolean mMinimized = false; + private volatile boolean mAdjustedForIme = false; + private boolean mHomeStackResizable = false; + + /** + * Creates {@link SplitScreen}, returns {@code null} if the feature is not supported. + */ + @Nullable + public static LegacySplitScreen create(Context context, + DisplayController displayController, SystemWindows systemWindows, + DisplayImeController imeController, TransactionPool transactionPool, + ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue, + TaskStackListenerImpl taskStackListener, Transitions transitions, + ShellExecutor mainExecutor, AnimationHandler sfVsyncAnimationHandler) { + return new LegacySplitScreenController(context, displayController, systemWindows, + imeController, transactionPool, shellTaskOrganizer, syncQueue, taskStackListener, + transitions, mainExecutor, sfVsyncAnimationHandler).mImpl; + } + + public LegacySplitScreenController(Context context, + DisplayController displayController, SystemWindows systemWindows, + DisplayImeController imeController, TransactionPool transactionPool, + ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue, + TaskStackListenerImpl taskStackListener, Transitions transitions, + ShellExecutor mainExecutor, AnimationHandler sfVsyncAnimationHandler) { + mContext = context; + mDisplayController = displayController; + mSystemWindows = systemWindows; + mImeController = imeController; + mMainExecutor = mainExecutor; + mSfVsyncAnimationHandler = sfVsyncAnimationHandler; + mForcedResizableController = new ForcedResizableInfoActivityController(context, this, + mainExecutor); + mTransactionPool = transactionPool; + mWindowManagerProxy = new WindowManagerProxy(syncQueue, shellTaskOrganizer); + mTaskOrganizer = shellTaskOrganizer; + mSplits = new LegacySplitScreenTaskListener(this, shellTaskOrganizer, transitions, + syncQueue); + mImePositionProcessor = new DividerImeController(mSplits, mTransactionPool, mMainExecutor, + shellTaskOrganizer); + mRotationController = + (display, fromRotation, toRotation, wct) -> { + if (!mSplits.isSplitScreenSupported() || mWindowManagerProxy == null) { + return; + } + WindowContainerTransaction t = new WindowContainerTransaction(); + DisplayLayout displayLayout = + new DisplayLayout(mDisplayController.getDisplayLayout(display)); + LegacySplitDisplayLayout sdl = + new LegacySplitDisplayLayout(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. + + taskStackListener.addListener( + new TaskStackListenerCallback() { + @Override + public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, + boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { + if (!wasVisible || task.getWindowingMode() + != WINDOWING_MODE_SPLIT_SCREEN_PRIMARY + || !mSplits.isSplitScreenSupported()) { + return; + } + + if (isMinimized()) { + onUndockingTask(); + } + } + + @Override + public void onActivityForcedResizable(String packageName, int taskId, + int reason) { + mForcedResizableController.activityForcedResizable(packageName, taskId, + reason); + } + + @Override + public void onActivityDismissingDockedStack() { + mForcedResizableController.activityDismissingSplitScreen(); + } + + @Override + public void onActivityLaunchOnSecondaryDisplayFailed() { + mForcedResizableController.activityLaunchOnSecondaryDisplayFailed(); + } + }); + } + + void onSplitScreenSupported() { + // 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); + } + + private 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 LegacySplitDisplayLayout(mDisplayController.getDisplayContext(displayId), + mDisplayController.getDisplayLayout(displayId), mSplits); + mImeController.addPositionProcessor(mImePositionProcessor); + mDisplayController.addDisplayChangingController(mRotationController); + if (!ActivityTaskManager.supportsSplitScreenMultiWindow(mContext)) { + removeDivider(); + return; + } + try { + mSplits.init(); + } 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 LegacySplitDisplayLayout(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); + } + } + + boolean isMinimized() { + return mMinimized; + } + + boolean isHomeStackResizable() { + return mHomeStackResizable; + } + + DividerView getDividerView() { + return mView; + } + + 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); + mView.setAnimationHandler(mSfVsyncAnimationHandler); + 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() { + 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; + }); + } + } + } + + private void setMinimized(final boolean minimized) { + if (DEBUG) Slog.d(TAG, "posting ext setMinimized " + minimized + " vis:" + mVisible); + mMainExecutor.execute(() -> { + 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); + } + + private void onUndockingTask() { + if (mView != null) { + mView.onUndockingTask(); + } + } + + private void onAppTransitionFinished() { + if (mView == null) { + return; + } + mForcedResizableController.onAppTransitionFinished(); + } + + private 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); + } + + void registerInSplitScreenListener(Consumer<Boolean> listener) { + listener.accept(isDividerVisible()); + synchronized (mDockedStackExistsListeners) { + mDockedStackExistsListeners.add(new WeakReference<>(listener)); + } + } + + void unregisterInSplitScreenListener(Consumer<Boolean> listener) { + synchronized (mDockedStackExistsListeners) { + for (int i = mDockedStackExistsListeners.size() - 1; i >= 0; i--) { + if (mDockedStackExistsListeners.get(i) == listener) { + mDockedStackExistsListeners.remove(i); + } + } + } + } + + private void registerBoundsChangeListener(BiConsumer<Rect, Rect> listener) { + synchronized (mBoundsChangedListeners) { + mBoundsChangedListeners.add(new WeakReference<>(listener)); + } + } + + private boolean splitPrimaryTask() { + try { + if (ActivityTaskManager.getService().getLockTaskModeState() == LOCK_TASK_MODE_PINNED + || isSplitActive()) { + return false; + } + } catch (RemoteException e) { + return false; + } + + // Try fetching the top running task. + final List<RunningTaskInfo> runningTasks = + ActivityTaskManager.getInstance().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.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.getInstance().setTaskWindowingModeSplitScreenPrimary( + topRunningTask.taskId, true /* onTop */); + } + + private void dismissSplitToPrimaryTask() { + startDismissSplit(true /* toPrimaryTask */); + } + + /** 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 prepareEnterSplitTransition(WindowContainerTransaction outWct) { + // Set resizable directly here because buildEnterSplit already resizes home stack. + mHomeStackResizable = mWindowManagerProxy.buildEnterSplit(outWct, mSplits, mSplitLayout); + } + + void finishEnterSplitTransition(boolean minimized) { + update(mDisplayController.getDisplayContext( + mContext.getDisplayId()).getResources().getConfiguration()); + if (minimized) { + ensureMinimizedSplit(); + } else { + ensureNormalSplit(); + } + } + + void startDismissSplit(boolean toPrimaryTask) { + startDismissSplit(toPrimaryTask, false /* snapped */); + } + + void startDismissSplit(boolean toPrimaryTask, boolean snapped) { + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mSplits.getSplitTransitions().dismissSplit( + mSplits, mSplitLayout, !toPrimaryTask, snapped); + } else { + mWindowManagerProxy.applyDismissSplit(mSplits, mSplitLayout, !toPrimaryTask); + onDismissSplit(); + } + } + + void onDismissSplit() { + 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 */); + } + } + + LegacySplitDisplayLayout getSplitLayout() { + return mSplitLayout; + } + + WindowManagerProxy getWmProxy() { + return mWindowManagerProxy; + } + + WindowContainerToken getSecondaryRoot() { + if (mSplits == null || mSplits.mSecondary == null) { + return null; + } + return mSplits.mSecondary.token; + } + + private class SplitScreenImpl implements LegacySplitScreen { + @Override + public boolean isMinimized() { + return mMinimized; + } + + @Override + public boolean isHomeStackResizable() { + return mHomeStackResizable; + } + + /** + * TODO: Remove usage from outside the shell. + */ + @Override + public DividerView getDividerView() { + return LegacySplitScreenController.this.getDividerView(); + } + + @Override + public boolean isDividerVisible() { + boolean[] result = new boolean[1]; + try { + mMainExecutor.executeBlocking(() -> { + result[0] = LegacySplitScreenController.this.isDividerVisible(); + }); + } catch (InterruptedException e) { + Slog.e(TAG, "Failed to get divider visible"); + } + return result[0]; + } + + @Override + public void onKeyguardVisibilityChanged(boolean isShowing) { + mMainExecutor.execute(() -> { + LegacySplitScreenController.this.onKeyguardVisibilityChanged(isShowing); + }); + } + + @Override + public void setMinimized(boolean minimized) { + mMainExecutor.execute(() -> { + LegacySplitScreenController.this.setMinimized(minimized); + }); + } + + @Override + public void onUndockingTask() { + mMainExecutor.execute(() -> { + LegacySplitScreenController.this.onUndockingTask(); + }); + } + + @Override + public void onAppTransitionFinished() { + mMainExecutor.execute(() -> { + LegacySplitScreenController.this.onAppTransitionFinished(); + }); + } + + @Override + public void registerInSplitScreenListener(Consumer<Boolean> listener) { + mMainExecutor.execute(() -> { + LegacySplitScreenController.this.registerInSplitScreenListener(listener); + }); + } + + @Override + public void unregisterInSplitScreenListener(Consumer<Boolean> listener) { + mMainExecutor.execute(() -> { + LegacySplitScreenController.this.unregisterInSplitScreenListener(listener); + }); + } + + @Override + public void registerBoundsChangeListener(BiConsumer<Rect, Rect> listener) { + mMainExecutor.execute(() -> { + LegacySplitScreenController.this.registerBoundsChangeListener(listener); + }); + } + + @Override + public WindowContainerToken getSecondaryRoot() { + WindowContainerToken[] result = new WindowContainerToken[1]; + try { + mMainExecutor.executeBlocking(() -> { + result[0] = LegacySplitScreenController.this.getSecondaryRoot(); + }); + } catch (InterruptedException e) { + Slog.e(TAG, "Failed to get secondary root"); + } + return result[0]; + } + + @Override + public boolean splitPrimaryTask() { + boolean[] result = new boolean[1]; + try { + mMainExecutor.executeBlocking(() -> { + result[0] = LegacySplitScreenController.this.splitPrimaryTask(); + }); + } catch (InterruptedException e) { + Slog.e(TAG, "Failed to split primary task"); + } + return result[0]; + } + + @Override + public void dismissSplitToPrimaryTask() { + mMainExecutor.execute(() -> { + LegacySplitScreenController.this.dismissSplitToPrimaryTask(); + }); + } + + @Override + public void dump(PrintWriter pw) { + try { + mMainExecutor.executeBlocking(() -> { + LegacySplitScreenController.this.dump(pw); + }); + } catch (InterruptedException e) { + Slog.e(TAG, "Failed to dump LegacySplitScreenController in 2s"); + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTaskListener.java new file mode 100644 index 000000000000..5a493c234ce3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTaskListener.java @@ -0,0 +1,339 @@ +/* + * 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.legacysplitscreen; + +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.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; + +import android.app.ActivityManager.RunningTaskInfo; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.Log; +import android.util.SparseArray; +import android.view.SurfaceControl; +import android.view.SurfaceSession; +import android.window.TaskOrganizer; + +import androidx.annotation.NonNull; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.transition.Transitions; + +import java.io.PrintWriter; +import java.util.ArrayList; + +class LegacySplitScreenTaskListener implements ShellTaskOrganizer.TaskListener { + private static final String TAG = LegacySplitScreenTaskListener.class.getSimpleName(); + private static final boolean DEBUG = LegacySplitScreenController.DEBUG; + + private final ShellTaskOrganizer mTaskOrganizer; + private final SyncTransactionQueue mSyncQueue; + private final SparseArray<SurfaceControl> mLeashByTaskId = new SparseArray<>(); + + RunningTaskInfo mPrimary; + RunningTaskInfo mSecondary; + SurfaceControl mPrimarySurface; + SurfaceControl mSecondarySurface; + SurfaceControl mPrimaryDim; + SurfaceControl mSecondaryDim; + Rect mHomeBounds = new Rect(); + final LegacySplitScreenController mSplitScreenController; + private boolean mSplitScreenSupported = false; + + final SurfaceSession mSurfaceSession = new SurfaceSession(); + + private final SplitScreenTransitions mSplitTransitions; + + LegacySplitScreenTaskListener(LegacySplitScreenController splitScreenController, + ShellTaskOrganizer shellTaskOrganizer, + Transitions transitions, + SyncTransactionQueue syncQueue) { + mSplitScreenController = splitScreenController; + mTaskOrganizer = shellTaskOrganizer; + mSplitTransitions = new SplitScreenTransitions(splitScreenController.mTransactionPool, + transitions, mSplitScreenController, this); + transitions.addHandler(mSplitTransitions); + mSyncQueue = syncQueue; + } + + void init() { + synchronized (this) { + try { + mTaskOrganizer.createRootTask( + DEFAULT_DISPLAY, WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, this); + mTaskOrganizer.createRootTask( + DEFAULT_DISPLAY, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, this); + } 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); + } + + TaskOrganizer getTaskOrganizer() { + return mTaskOrganizer; + } + + SplitScreenTransitions getSplitTransitions() { + return mSplitTransitions; + } + + @Override + public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { + synchronized (this) { + if (taskInfo.hasParentTask()) { + handleChildTaskAppeared(taskInfo, leash); + return; + } + + final int winMode = taskInfo.getWindowingMode(); + if (winMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) { + ProtoLog.v(WM_SHELL_TASK_ORG, + "%s onTaskAppeared Primary taskId=%d", TAG, taskInfo.taskId); + mPrimary = taskInfo; + mPrimarySurface = leash; + } else if (winMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) { + ProtoLog.v(WM_SHELL_TASK_ORG, + "%s onTaskAppeared Secondary taskId=%d", TAG, taskInfo.taskId); + mSecondary = taskInfo; + mSecondarySurface = leash; + } else { + ProtoLog.v(WM_SHELL_TASK_ORG, "%s onTaskAppeared unknown taskId=%d winMode=%d", + TAG, taskInfo.taskId, winMode); + } + + if (!mSplitScreenSupported && mPrimarySurface != null && mSecondarySurface != null) { + mSplitScreenSupported = true; + mSplitScreenController.onSplitScreenSupported(); + ProtoLog.v(WM_SHELL_TASK_ORG, "%s onTaskAppeared Supported", TAG); + + // 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) { + if (taskInfo.hasParentTask()) { + mLeashByTaskId.remove(taskInfo.taskId); + return; + } + + 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; + } + synchronized (this) { + if (taskInfo.hasParentTask()) { + handleChildTaskChanged(taskInfo); + return; + } + + handleTaskInfoChanged(taskInfo); + } + } + + private void handleChildTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { + mLeashByTaskId.put(taskInfo.taskId, leash); + if (Transitions.ENABLE_SHELL_TRANSITIONS) return; + updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */); + } + + private void handleChildTaskChanged(RunningTaskInfo taskInfo) { + if (Transitions.ENABLE_SHELL_TRANSITIONS) return; + final SurfaceControl leash = mLeashByTaskId.get(taskInfo.taskId); + updateChildTaskSurface(taskInfo, leash, false /* firstAppeared */); + } + + private void updateChildTaskSurface( + RunningTaskInfo taskInfo, SurfaceControl leash, boolean firstAppeared) { + final Point taskPositionInParent = taskInfo.positionInParent; + mSyncQueue.runInSync(t -> { + t.setWindowCrop(leash, null); + t.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y); + if (firstAppeared && !Transitions.ENABLE_SHELL_TRANSITIONS) { + t.setAlpha(leash, 1f); + t.setMatrix(leash, 1, 0, 0, 1); + t.show(leash); + } + }); + } + + /** + * 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; + } + if (DEBUG) { + Log.d(TAG, "onTaskInfoChanged " + mPrimary + " " + mSecondary); + } + if (Transitions.ENABLE_SHELL_TRANSITIONS) return; + 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 (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(false /* toPrimaryTask */); + } 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) { + // Workaround for b/172686383, we can't rely on the sync bounds change transaction for + // the home task to finish before the last updateChildTaskSurface() call even if it's + // queued on the sync transaction queue, so ensure that the home task surface is updated + // again before we minimize + final ArrayList<RunningTaskInfo> tasks = new ArrayList<>(); + mSplitScreenController.getWmProxy().getHomeAndRecentsTasks(tasks, + mSplitScreenController.getSecondaryRoot()); + for (int i = 0; i < tasks.size(); i++) { + final RunningTaskInfo taskInfo = tasks.get(i); + final SurfaceControl leash = mLeashByTaskId.get(taskInfo.taskId); + if (leash != null) { + updateChildTaskSurface(taskInfo, leash, false /* firstAppeared */); + } + } + + // 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); + pw.println(innerPrefix + "mSplitScreenSupported=" + mSplitScreenSupported); + if (mPrimary != null) pw.println(innerPrefix + "mPrimary.taskId=" + mPrimary.taskId); + if (mSecondary != null) pw.println(innerPrefix + "mSecondary.taskId=" + mSecondary.taskId); + } + + @Override + public String toString() { + return TAG; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/MinimizedDockShadow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/MinimizedDockShadow.java new file mode 100644 index 000000000000..1e9223cbe3e2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/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.legacysplitscreen; + +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/legacysplitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/SplitScreenTransitions.java new file mode 100644 index 000000000000..eea5c08818cc --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/SplitScreenTransitions.java @@ -0,0 +1,346 @@ +/* + * 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.legacysplitscreen; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.WindowConfiguration; +import android.graphics.Rect; +import android.os.IBinder; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.transition.Transitions; + +import java.util.ArrayList; + +/** Plays transition animations for split-screen */ +public class SplitScreenTransitions implements Transitions.TransitionHandler { + private static final String TAG = "SplitScreenTransitions"; + + public static final int TRANSIT_SPLIT_DISMISS_SNAP = TRANSIT_FIRST_CUSTOM + 10; + + private final TransactionPool mTransactionPool; + private final Transitions mTransitions; + private final LegacySplitScreenController mSplitScreen; + private final LegacySplitScreenTaskListener mListener; + + private IBinder mPendingDismiss = null; + private boolean mDismissFromSnap = false; + private IBinder mPendingEnter = null; + private IBinder mAnimatingTransition = null; + + /** Keeps track of currently running animations */ + private final ArrayList<Animator> mAnimations = new ArrayList<>(); + + private Transitions.TransitionFinishCallback mFinishCallback = null; + private SurfaceControl.Transaction mFinishTransaction; + + SplitScreenTransitions(@NonNull TransactionPool pool, @NonNull Transitions transitions, + @NonNull LegacySplitScreenController splitScreen, + @NonNull LegacySplitScreenTaskListener listener) { + mTransactionPool = pool; + mTransitions = transitions; + mSplitScreen = splitScreen; + mListener = listener; + } + + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @Nullable TransitionRequestInfo request) { + WindowContainerTransaction out = null; + final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask(); + final @WindowManager.TransitionType int type = request.getType(); + if (mSplitScreen.isDividerVisible()) { + // try to handle everything while in split-screen + out = new WindowContainerTransaction(); + if (triggerTask != null) { + final boolean shouldDismiss = + // if we close the primary-docked task, then leave split-screen since there + // is nothing behind it. + ((type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK) + && triggerTask.parentTaskId == mListener.mPrimary.taskId) + // if a non-resizable is launched, we also need to leave split-screen. + || ((type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT) + && !triggerTask.isResizeable); + // In both cases, dismiss the primary + if (shouldDismiss) { + WindowManagerProxy.buildDismissSplit(out, mListener, + mSplitScreen.getSplitLayout(), true /* dismiss */); + if (type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT) { + out.reorder(triggerTask.token, true /* onTop */); + } + mPendingDismiss = transition; + } + } + } else if (triggerTask != null) { + // Not in split mode, so look for an open with a trigger task. + if ((type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT) + && triggerTask.configuration.windowConfiguration.getWindowingMode() + == WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) { + out = new WindowContainerTransaction(); + mSplitScreen.prepareEnterSplitTransition(out); + mPendingEnter = transition; + } + } + return out; + } + + // TODO(shell-transitions): real animations + private void startExampleAnimation(@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); + mTransitions.getMainExecutor().execute(() -> { + mAnimations.remove(va); + onFinish(); + }); + }; + 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) { } + }); + mAnimations.add(va); + mTransitions.getAnimExecutor().execute(va::start); + } + + // TODO(shell-transitions): real animations + private void startExampleResizeAnimation(@NonNull SurfaceControl leash, + @NonNull Rect startBounds, @NonNull Rect endBounds) { + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + final ValueAnimator va = ValueAnimator.ofFloat(0.f, 1.f); + va.setDuration(500); + va.addUpdateListener(animation -> { + float fraction = animation.getAnimatedFraction(); + transaction.setWindowCrop(leash, + (int) (startBounds.width() * (1.f - fraction) + endBounds.width() * fraction), + (int) (startBounds.height() * (1.f - fraction) + + endBounds.height() * fraction)); + transaction.setPosition(leash, + startBounds.left * (1.f - fraction) + endBounds.left * fraction, + startBounds.top * (1.f - fraction) + endBounds.top * fraction); + transaction.apply(); + }); + final Runnable finisher = () -> { + transaction.setWindowCrop(leash, 0, 0); + transaction.setPosition(leash, endBounds.left, endBounds.top); + transaction.apply(); + mTransactionPool.release(transaction); + mTransitions.getMainExecutor().execute(() -> { + mAnimations.remove(va); + onFinish(); + }); + }; + va.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + finisher.run(); + } + + @Override + public void onAnimationCancel(Animator animation) { + finisher.run(); + } + }); + mAnimations.add(va); + mTransitions.getAnimExecutor().execute(va::start); + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (transition != mPendingDismiss && transition != mPendingEnter) { + // If we're not in split-mode, just abort + if (!mSplitScreen.isDividerVisible()) return false; + // Check to see if HOME is involved + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (change.getTaskInfo() == null + || change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME) continue; + if (change.getMode() == TRANSIT_OPEN || change.getMode() == TRANSIT_TO_FRONT) { + mSplitScreen.ensureMinimizedSplit(); + } else if (change.getMode() == TRANSIT_CLOSE + || change.getMode() == TRANSIT_TO_BACK) { + mSplitScreen.ensureNormalSplit(); + } + } + // Use normal animations. + return false; + } + + mFinishCallback = finishCallback; + mFinishTransaction = mTransactionPool.acquire(); + mAnimatingTransition = transition; + + // Play fade animations + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + final SurfaceControl leash = change.getLeash(); + final int mode = info.getChanges().get(i).getMode(); + + if (mode == TRANSIT_CHANGE) { + if (change.getParent() != null) { + // This is probably reparented, so we want the parent to be immediately visible + final TransitionInfo.Change parentChange = info.getChange(change.getParent()); + t.show(parentChange.getLeash()); + t.setAlpha(parentChange.getLeash(), 1.f); + // and then animate this layer outside the parent (since, for example, this is + // the home task animating from fullscreen to part-screen). + t.reparent(leash, info.getRootLeash()); + t.setLayer(leash, info.getChanges().size() - i); + // build the finish reparent/reposition + mFinishTransaction.reparent(leash, parentChange.getLeash()); + mFinishTransaction.setPosition(leash, + change.getEndRelOffset().x, change.getEndRelOffset().y); + } + // TODO(shell-transitions): screenshot here + final Rect startBounds = new Rect(change.getStartAbsBounds()); + final boolean isHome = change.getTaskInfo() != null + && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME; + if (mPendingDismiss == transition && mDismissFromSnap && !isHome) { + // Home is special since it doesn't move during fling. Everything else, though, + // when dismissing from snap, the top/left is at 0,0. + startBounds.offsetTo(0, 0); + } + final Rect endBounds = new Rect(change.getEndAbsBounds()); + startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); + endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); + startExampleResizeAnimation(leash, startBounds, endBounds); + } + if (change.getParent() != null) { + continue; + } + + if (transition == mPendingEnter + && mListener.mPrimary.token.equals(change.getContainer()) + || mListener.mSecondary.token.equals(change.getContainer())) { + t.setWindowCrop(leash, change.getStartAbsBounds().width(), + change.getStartAbsBounds().height()); + if (mListener.mPrimary.token.equals(change.getContainer())) { + // Move layer to top since we want it above the oversized home task during + // animation even though home task is on top in hierarchy. + t.setLayer(leash, info.getChanges().size() + 1); + } + } + boolean isOpening = Transitions.isOpeningType(info.getType()); + if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) { + // fade in + startExampleAnimation(leash, true /* show */); + } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) { + // fade out + if (transition == mPendingDismiss && mDismissFromSnap) { + // Dismissing via snap-to-top/bottom means that the dismissed task is already + // not-visible (usually cropped to oblivion) so immediately set its alpha to 0 + // and don't animate it so it doesn't pop-in when reparented. + t.setAlpha(leash, 0.f); + } else { + startExampleAnimation(leash, false /* show */); + } + } + } + if (transition == mPendingEnter) { + // If entering, check if we should enter into minimized or normal split + boolean homeIsVisible = false; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (change.getTaskInfo() == null + || change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME) { + continue; + } + homeIsVisible = change.getMode() == TRANSIT_OPEN + || change.getMode() == TRANSIT_TO_FRONT + || change.getMode() == TRANSIT_CHANGE; + break; + } + mSplitScreen.finishEnterSplitTransition(homeIsVisible); + } + t.apply(); + onFinish(); + return true; + } + + @ExternalThread + void dismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, + boolean dismissOrMaximize, boolean snapped) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + WindowManagerProxy.buildDismissSplit(wct, tiles, layout, dismissOrMaximize); + mTransitions.getMainExecutor().execute(() -> { + mDismissFromSnap = snapped; + mPendingDismiss = mTransitions.startTransition(TRANSIT_SPLIT_DISMISS_SNAP, wct, this); + }); + } + + private void onFinish() { + if (!mAnimations.isEmpty()) return; + mFinishTransaction.apply(); + mTransactionPool.release(mFinishTransaction); + mFinishTransaction = null; + mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); + mFinishCallback = null; + if (mAnimatingTransition == mPendingEnter) { + mPendingEnter = null; + } + if (mAnimatingTransition == mPendingDismiss) { + mSplitScreen.onDismissSplit(); + mPendingDismiss = null; + } + mDismissFromSnap = false; + mAnimatingTransition = null; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/WindowManagerProxy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/WindowManagerProxy.java new file mode 100644 index 000000000000..94c6f018b6ac --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/WindowManagerProxy.java @@ -0,0 +1,383 @@ +/* + * 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.legacysplitscreen; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; +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 com.android.wm.shell.transition.Transitions; + +import java.util.ArrayList; +import java.util.List; + +/** + * 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}; + private static final int[] CONTROLLED_ACTIVITY_TYPES = { + ACTIVITY_TYPE_STANDARD, + ACTIVITY_TYPE_HOME, + ACTIVITY_TYPE_RECENTS, + ACTIVITY_TYPE_UNDEFINED + }; + private static final int[] CONTROLLED_WINDOWING_MODES = { + WINDOWING_MODE_FULLSCREEN, + WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, + WINDOWING_MODE_UNDEFINED + }; + + @GuardedBy("mDockedRect") + private final Rect mDockedRect = new Rect(); + + private final Rect mTmpRect1 = new Rect(); + + @GuardedBy("mDockedRect") + private final Rect mTouchableRegion = new Rect(); + + private final SyncTransactionQueue mSyncTransactionQueue; + private final TaskOrganizer mTaskOrganizer; + + WindowManagerProxy(SyncTransactionQueue syncQueue, TaskOrganizer taskOrganizer) { + mSyncTransactionQueue = syncQueue; + mTaskOrganizer = taskOrganizer; + } + + void dismissOrMaximizeDocked(final LegacySplitScreenTaskListener tiles, + LegacySplitDisplayLayout layout, final boolean dismissOrMaximize) { + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + tiles.mSplitScreenController.startDismissSplit(!dismissOrMaximize, true /* snapped */); + } else { + applyDismissSplit(tiles, layout, dismissOrMaximize); + } + } + + public void setResizing(final boolean resizing) { + try { + ActivityTaskManager.getService().setSplitScreenResizing(resizing); + } catch (RemoteException e) { + Log.w(TAG, "Error calling setDockedStackResizing: " + e); + } + } + + /** Sets a touch region */ + public void setTouchRegion(Rect region) { + try { + synchronized (mDockedRect) { + mTouchableRegion.set(region); + } + WindowManagerGlobal.getWindowManagerService().setDockedStackDividerTouchRegion( + mTouchableRegion); + } catch (RemoteException e) { + Log.w(TAG, "Failed to set touchable region: " + e); + } + } + + void applyResizeSplits(int position, LegacySplitDisplayLayout splitLayout) { + WindowContainerTransaction t = new WindowContainerTransaction(); + splitLayout.resizeSplits(position, t); + new WindowOrganizer().applyTransaction(t); + } + + 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(LegacySplitDisplayLayout 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; + } + + /** @see #buildEnterSplit */ + boolean applyEnterSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout) { + // Set launchtile first so that any stack created after + // getAllRootTaskInfos and before reparent (even if unlikely) are placed + // correctly. + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setLaunchRoot(tiles.mSecondary.token, CONTROLLED_WINDOWING_MODES, + CONTROLLED_ACTIVITY_TYPES); + final boolean isHomeResizable = buildEnterSplit(wct, tiles, layout); + applySyncTransaction(wct); + 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 buildEnterSplit(WindowContainerTransaction outWct, LegacySplitScreenTaskListener tiles, + LegacySplitDisplayLayout layout) { + List<ActivityManager.RunningTaskInfo> rootTasks = + mTaskOrganizer.getRootTasks(DEFAULT_DISPLAY, null /* activityTypes */); + 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.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; + outWct.reparent(rootTask.token, tiles.mSecondary.token, true /* onTop */); + } + // Move the secondary split-forward. + outWct.reorder(tiles.mSecondary.token, true /* onTop */); + boolean isHomeResizable = applyHomeTasksMinimized(layout, null /* parent */, + outWct); + if (topHomeTask != null && !Transitions.ENABLE_SHELL_TRANSITIONS) { + // 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. + outWct.setBoundsChangeTransaction(topHomeTask.token, tiles.mHomeBounds); + } + return isHomeResizable; + } + + static boolean isHomeOrRecentTask(ActivityManager.RunningTaskInfo ti) { + final int atype = ti.getActivityType(); + return atype == ACTIVITY_TYPE_HOME || atype == ACTIVITY_TYPE_RECENTS; + } + + /** @see #buildDismissSplit */ + void applyDismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, + boolean dismissOrMaximize) { + // TODO(task-org): Once task-org is more complete, consider using Appeared/Vanished + // plus specific APIs to clean this up. + final WindowContainerTransaction wct = new WindowContainerTransaction(); + // Set launch root first so that any task created after getChildContainers and + // before reparent (pretty unlikely) are put into fullscreen. + wct.setLaunchRoot(tiles.mSecondary.token, null, null); + buildDismissSplit(wct, tiles, layout, dismissOrMaximize); + applySyncTransaction(wct); + } + + /** + * 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. + */ + static void buildDismissSplit(WindowContainerTransaction outWct, + LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, + boolean dismissOrMaximize) { + // TODO(task-org): Once task-org is more complete, consider using Appeared/Vanished + // plus specific APIs to clean this up. + final TaskOrganizer taskOrg = tiles.getTaskOrganizer(); + List<ActivityManager.RunningTaskInfo> primaryChildren = + taskOrg.getChildTasks(tiles.mPrimary.token, null /* activityTypes */); + List<ActivityManager.RunningTaskInfo> secondaryChildren = + taskOrg.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 = + taskOrg.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; + } + if (dismissOrMaximize) { + // Dismissing, so move all primary split tasks first + for (int i = primaryChildren.size() - 1; i >= 0; --i) { + outWct.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); + outWct.reparent(ti.token, null /* parent */, true /* onTop */); + if (isHomeOrRecentTask(ti)) { + outWct.setBounds(ti.token, null); + outWct.setWindowingMode(ti.token, WINDOWING_MODE_UNDEFINED); + if (i == 0) { + homeOnTop = true; + } + } + } + if (homeOnTop && !Transitions.ENABLE_SHELL_TRANSITIONS) { + // 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); + outWct.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; + } + outWct.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)) { + outWct.reparent(ti.token, null /* parent */, true /* onTop */); + // reset bounds and mode too + outWct.setBounds(ti.token, null); + outWct.setWindowingMode(ti.token, WINDOWING_MODE_UNDEFINED); + } + } + for (int i = primaryChildren.size() - 1; i >= 0; --i) { + outWct.reparent(primaryChildren.get(i).token, null /* parent */, + true /* onTop */); + } + } + for (int i = freeHomeAndRecents.size() - 1; i >= 0; --i) { + outWct.setBounds(freeHomeAndRecents.get(i).token, null); + outWct.setWindowingMode(freeHomeAndRecents.get(i).token, WINDOWING_MODE_UNDEFINED); + } + // Reset focusable to true + outWct.setFocusable(tiles.mPrimary.token, true /* focusable */); + } + + /** + * 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/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..e95864873c0c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java @@ -0,0 +1,77 @@ +/* + * 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.common.annotations.ExternalThread; +import com.android.wm.shell.onehanded.OneHandedGestureHandler.OneHandedGestureEventCallback; + +import java.io.PrintWriter; + +/** + * Interface to engage one handed feature. + */ +@ExternalThread +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 OneHandedUiEventLogger}. + */ + void stopOneHanded(int uiEvent); + + /** + * 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..125e322974bf --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationController.java @@ -0,0 +1,300 @@ +/* + * 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 android.window.WindowContainerToken; + +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<WindowContainerToken, OneHandedTransitionAnimator> mAnimatorMap = + new HashMap<>(); + + /** + * Constructor of OneHandedAnimationController + */ + public OneHandedAnimationController(Context context) { + mSurfaceTransactionHelper = new OneHandedSurfaceTransactionHelper(context); + mOvershootInterpolator = new OvershootInterpolator(); + } + + @SuppressWarnings("unchecked") + OneHandedTransitionAnimator getAnimator(WindowContainerToken token, SurfaceControl leash, + Rect startBounds, Rect endBounds) { + final OneHandedTransitionAnimator animator = mAnimatorMap.get(token); + if (animator == null) { + mAnimatorMap.put(token, setupOneHandedTransitionAnimator( + OneHandedTransitionAnimator.ofBounds(token, leash, startBounds, endBounds))); + } else if (animator.isRunning()) { + animator.updateEndValue(endBounds); + } else { + animator.cancel(); + mAnimatorMap.put(token, setupOneHandedTransitionAnimator( + OneHandedTransitionAnimator.ofBounds(token, leash, startBounds, endBounds))); + } + return mAnimatorMap.get(token); + } + + HashMap<WindowContainerToken, OneHandedTransitionAnimator> getAnimatorMap() { + return mAnimatorMap; + } + + boolean isAnimatorsConsumed() { + return mAnimatorMap.isEmpty(); + } + + void removeAnimator(WindowContainerToken token) { + final OneHandedTransitionAnimator animator = mAnimatorMap.remove(token); + 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 final WindowContainerToken mToken; + 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 OneHandedTransitionAnimator(WindowContainerToken token, SurfaceControl leash, + T startValue, T endValue) { + mLeash = leash; + mToken = token; + 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> addOneHandedAnimationCallback( + OneHandedAnimationCallback callback) { + mOneHandedAnimationCallbacks.add(callback); + return this; + } + + WindowContainerToken getToken() { + return mToken; + } + + 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; + } + + 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(WindowContainerToken token, + SurfaceControl leash, Rect startValue, Rect endValue) { + + return new OneHandedTransitionAnimator<Rect>(token, 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/OneHandedBackgroundPanelOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedBackgroundPanelOrganizer.java new file mode 100644 index 000000000000..37a91d0c121c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedBackgroundPanelOrganizer.java @@ -0,0 +1,201 @@ +/* + * 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.content.Context; +import android.content.res.Resources; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Handler; +import android.util.Log; +import android.view.SurfaceControl; +import android.view.SurfaceSession; +import android.window.DisplayAreaAppearedInfo; +import android.window.DisplayAreaInfo; +import android.window.DisplayAreaOrganizer; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.internal.annotations.GuardedBy; +import com.android.wm.shell.R; +import com.android.wm.shell.common.DisplayController; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Manages OneHanded color background layer areas. + * To avoid when turning the Dark theme on, users can not clearly identify + * the screen has entered one handed mode. + */ +public class OneHandedBackgroundPanelOrganizer extends DisplayAreaOrganizer + implements OneHandedTransitionCallback { + private static final String TAG = "OneHandedBackgroundPanelOrganizer"; + + private final Object mLock = new Object(); + private final SurfaceSession mSurfaceSession = new SurfaceSession(); + private final float[] mColor; + private final float mAlpha; + private final Rect mRect; + private final Executor mMainExecutor; + private final Point mDisplaySize = new Point(); + private final OneHandedSurfaceTransactionHelper.SurfaceControlTransactionFactory + mSurfaceControlTransactionFactory; + + @VisibleForTesting + @GuardedBy("mLock") + boolean mIsShowing; + @Nullable + @GuardedBy("mLock") + private SurfaceControl mBackgroundSurface; + @Nullable + @GuardedBy("mLock") + private SurfaceControl mParentLeash; + + private final OneHandedAnimationCallback mOneHandedAnimationCallback = + new OneHandedAnimationCallback() { + @Override + public void onOneHandedAnimationStart( + OneHandedAnimationController.OneHandedTransitionAnimator animator) { + mMainExecutor.execute(() -> showBackgroundPanelLayer()); + } + }; + + @Override + public void onStopFinished(Rect bounds) { + mMainExecutor.execute(() -> removeBackgroundPanelLayer()); + } + + public OneHandedBackgroundPanelOrganizer(Context context, DisplayController displayController, + Executor executor) { + super(executor); + displayController.getDisplay(DEFAULT_DISPLAY).getRealSize(mDisplaySize); + final Resources res = context.getResources(); + final float defaultRGB = res.getFloat(R.dimen.config_one_handed_background_rgb); + mColor = new float[]{defaultRGB, defaultRGB, defaultRGB}; + mAlpha = res.getFloat(R.dimen.config_one_handed_background_alpha); + mRect = new Rect(0, 0, mDisplaySize.x, mDisplaySize.y); + mMainExecutor = executor; + mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; + } + + @Override + public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo, + @NonNull SurfaceControl leash) { + synchronized (mLock) { + if (mParentLeash == null) { + mParentLeash = leash; + } else { + throw new RuntimeException("There should be only one DisplayArea for " + + "the one-handed mode background panel"); + } + } + } + + OneHandedAnimationCallback getOneHandedAnimationCallback() { + return mOneHandedAnimationCallback; + } + + @Override + public List<DisplayAreaAppearedInfo> registerOrganizer(int displayAreaFeature) { + synchronized (mLock) { + final List<DisplayAreaAppearedInfo> displayAreaInfos; + displayAreaInfos = super.registerOrganizer(displayAreaFeature); + for (int i = 0; i < displayAreaInfos.size(); i++) { + final DisplayAreaAppearedInfo info = displayAreaInfos.get(i); + onDisplayAreaAppeared(info.getDisplayAreaInfo(), info.getLeash()); + } + return displayAreaInfos; + } + } + + @Override + public void unregisterOrganizer() { + synchronized (mLock) { + super.unregisterOrganizer(); + mParentLeash = null; + } + } + + @Nullable + @VisibleForTesting + SurfaceControl getBackgroundSurface() { + synchronized (mLock) { + if (mParentLeash == null) { + return null; + } + + if (mBackgroundSurface == null) { + mBackgroundSurface = new SurfaceControl.Builder(mSurfaceSession) + .setParent(mParentLeash) + .setColorLayer() + .setFormat(PixelFormat.RGBA_8888) + .setOpaque(false) + .setName("one-handed-background-panel") + .setCallsite("OneHandedBackgroundPanelOrganizer") + .build(); + } + return mBackgroundSurface; + } + } + + @VisibleForTesting + void showBackgroundPanelLayer() { + synchronized (mLock) { + if (mIsShowing) { + return; + } + + if (getBackgroundSurface() == null) { + Log.w(TAG, "mBackgroundSurface is null !"); + return; + } + + SurfaceControl.Transaction transaction = + mSurfaceControlTransactionFactory.getTransaction(); + transaction.setLayer(mBackgroundSurface, -1 /* at bottom-most layer */) + .setColor(mBackgroundSurface, mColor) + .setAlpha(mBackgroundSurface, mAlpha) + .show(mBackgroundSurface) + .apply(); + transaction.close(); + mIsShowing = true; + } + } + + @VisibleForTesting + void removeBackgroundPanelLayer() { + synchronized (mLock) { + if (mBackgroundSurface == null) { + return; + } + + SurfaceControl.Transaction transaction = + mSurfaceControlTransactionFactory.getTransaction(); + transaction.remove(mBackgroundSurface); + transaction.apply(); + transaction.close(); + mBackgroundSurface = null; + mIsShowing = false; + } + } +} 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..eaa704f22410 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java @@ -0,0 +1,572 @@ +/* + * 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.ComponentName; +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.RemoteException; +import android.os.ServiceManager; +import android.os.SystemProperties; +import android.provider.Settings; +import android.util.Slog; +import android.view.accessibility.AccessibilityManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.internal.logging.UiEventLogger; +import com.android.wm.shell.R; +import com.android.wm.shell.common.DisplayChangeController; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TaskStackListenerCallback; +import com.android.wm.shell.common.TaskStackListenerImpl; +import com.android.wm.shell.common.annotations.ExternalThread; +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 { + 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 volatile boolean mIsOneHandedEnabled; + private volatile 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 OneHandedUiEventLogger mOneHandedUiEventLogger; + private final TaskStackListenerImpl mTaskStackListener; + private final IOverlayManager mOverlayManager; + private final ShellExecutor mMainExecutor; + private final Handler mMainHandler; + private final OneHandedImpl mImpl = new OneHandedImpl(); + + private OneHandedDisplayAreaOrganizer mDisplayAreaOrganizer; + private final AccessibilityManager mAccessibilityManager; + private OneHandedBackgroundPanelOrganizer mBackgroundPanelOrganizer; + + /** + * 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; + private final ContentObserver mTimeoutObserver; + private final ContentObserver mTaskChangeExitObserver; + private final ContentObserver mSwipeToNotificationEnabledObserver; + + private AccessibilityManager.AccessibilityStateChangeListener + mAccessibilityStateChangeListener = + new AccessibilityManager.AccessibilityStateChangeListener() { + @Override + public void onAccessibilityStateChanged(boolean enabled) { + if (enabled) { + final int mOneHandedTimeout = OneHandedSettingsUtil + .getSettingsOneHandedModeTimeout(mContext.getContentResolver()); + final int timeout = mAccessibilityManager + .getRecommendedTimeoutMillis(mOneHandedTimeout * 1000 + /* align with A11y timeout millis */, + AccessibilityManager.FLAG_CONTENT_CONTROLS); + mTimeoutHandler.setTimeout(timeout / 1000); + } else { + mTimeoutHandler.setTimeout(OneHandedSettingsUtil + .getSettingsOneHandedModeTimeout(mContext.getContentResolver())); + } + } + }; + + private final TaskStackListenerCallback mTaskStackListenerCallback = + new TaskStackListenerCallback() { + @Override + public void onTaskCreated(int taskId, ComponentName componentName) { + stopOneHanded(OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_APP_TAPS_OUT); + } + + @Override + public void onTaskMovedToFront(int taskId) { + stopOneHanded(OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_APP_TAPS_OUT); + } + }; + + + /** + * Creates {@link OneHanded}, returns {@code null} if the feature is not supported. + */ + @Nullable + public static OneHanded create( + Context context, DisplayController displayController, + TaskStackListenerImpl taskStackListener, UiEventLogger uiEventLogger, + ShellExecutor mainExecutor, Handler mainHandler) { + if (!SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false)) { + Slog.w(TAG, "Device doesn't support OneHanded feature"); + return null; + } + + OneHandedTimeoutHandler timeoutHandler = new OneHandedTimeoutHandler(mainExecutor); + OneHandedTutorialHandler tutorialHandler = new OneHandedTutorialHandler(context, + mainExecutor); + OneHandedAnimationController animationController = + new OneHandedAnimationController(context); + OneHandedTouchHandler touchHandler = new OneHandedTouchHandler(timeoutHandler, + mainExecutor); + OneHandedGestureHandler gestureHandler = new OneHandedGestureHandler( + context, displayController, mainExecutor); + OneHandedBackgroundPanelOrganizer oneHandedBackgroundPanelOrganizer = + new OneHandedBackgroundPanelOrganizer(context, displayController, mainExecutor); + OneHandedDisplayAreaOrganizer organizer = new OneHandedDisplayAreaOrganizer( + context, displayController, animationController, tutorialHandler, + oneHandedBackgroundPanelOrganizer, mainExecutor); + OneHandedUiEventLogger oneHandedUiEventsLogger = new OneHandedUiEventLogger(uiEventLogger); + IOverlayManager overlayManager = IOverlayManager.Stub.asInterface( + ServiceManager.getService(Context.OVERLAY_SERVICE)); + return new OneHandedController(context, displayController, + oneHandedBackgroundPanelOrganizer, organizer, touchHandler, tutorialHandler, + gestureHandler, timeoutHandler, oneHandedUiEventsLogger, overlayManager, + taskStackListener, mainExecutor, mainHandler).mImpl; + } + + @VisibleForTesting + OneHandedController(Context context, + DisplayController displayController, + OneHandedBackgroundPanelOrganizer backgroundPanelOrganizer, + OneHandedDisplayAreaOrganizer displayAreaOrganizer, + OneHandedTouchHandler touchHandler, + OneHandedTutorialHandler tutorialHandler, + OneHandedGestureHandler gestureHandler, + OneHandedTimeoutHandler timeoutHandler, + OneHandedUiEventLogger uiEventsLogger, + IOverlayManager overlayManager, + TaskStackListenerImpl taskStackListener, + ShellExecutor mainExecutor, + Handler mainHandler) { + mContext = context; + mBackgroundPanelOrganizer = backgroundPanelOrganizer; + mDisplayAreaOrganizer = displayAreaOrganizer; + mDisplayController = displayController; + mTouchHandler = touchHandler; + mTutorialHandler = tutorialHandler; + mGestureHandler = gestureHandler; + mOverlayManager = overlayManager; + mMainExecutor = mainExecutor; + mMainHandler = mainHandler; + mOneHandedUiEventLogger = uiEventsLogger; + mTaskStackListener = taskStackListener; + + final float offsetPercentageConfig = context.getResources().getFraction( + R.fraction.config_one_handed_offset, 1, 1); + final int sysPropPercentageConfig = SystemProperties.getInt( + ONE_HANDED_MODE_OFFSET_PERCENTAGE, Math.round(offsetPercentageConfig * 100.0f)); + mOffSetFraction = sysPropPercentageConfig / 100.0f; + mIsOneHandedEnabled = OneHandedSettingsUtil.getSettingsOneHandedModeEnabled( + context.getContentResolver()); + mIsSwipeToNotificationEnabled = + OneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled( + context.getContentResolver()); + mTimeoutHandler = timeoutHandler; + + mEnabledObserver = getObserver(this::onEnabledSettingChanged); + mTimeoutObserver = getObserver(this::onTimeoutSettingChanged); + mTaskChangeExitObserver = getObserver(this::onTaskChangeExitSettingChanged); + mSwipeToNotificationEnabledObserver = + getObserver(this::onSwipeToNotificationEnabledSettingChanged); + + mDisplayController.addDisplayChangingController(mRotationController); + + setupCallback(); + setupSettingObservers(); + setupTimeoutListener(); + setupGesturalOverlay(); + updateSettings(); + + mAccessibilityManager = (AccessibilityManager) + context.getSystemService(Context.ACCESSIBILITY_SERVICE); + mAccessibilityManager.addAccessibilityStateChangeListener( + mAccessibilityStateChangeListener); + } + + /** + * 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) { + if (enabled) { + mTaskStackListener.addListener(mTaskStackListenerCallback); + } else { + mTaskStackListener.removeListener(mTaskStackListenerCallback); + } + mTaskChangeToExit = enabled; + } + + /** + * Sets whether to enable swipe bottom to notification gesture when user update settings. + */ + void setSwipeToNotificationEnabled(boolean enabled) { + mIsSwipeToNotificationEnabled = enabled; + updateOneHandedEnabled(); + } + + @VisibleForTesting + void startOneHanded() { + if (!mDisplayAreaOrganizer.isInOneHanded()) { + final int yOffSet = Math.round(getDisplaySize().y * mOffSetFraction); + mDisplayAreaOrganizer.scheduleOffset(0, yOffSet); + mTimeoutHandler.resetTimer(); + + mOneHandedUiEventLogger.writeEvent( + OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_GESTURE_IN); + } + } + + @VisibleForTesting + void stopOneHanded() { + if (mDisplayAreaOrganizer.isInOneHanded()) { + mDisplayAreaOrganizer.scheduleOffset(0, 0); + mTimeoutHandler.removeTimer(); + } + } + + private void stopOneHanded(int uiEvent) { + if (mDisplayAreaOrganizer.isInOneHanded()) { + mDisplayAreaOrganizer.scheduleOffset(0, 0); + mTimeoutHandler.removeTimer(); + mOneHandedUiEventLogger.writeEvent(uiEvent); + } + } + + private void setThreeButtonModeEnabled(boolean enabled) { + mGestureHandler.onThreeButtonModeEnabled(enabled); + } + + @VisibleForTesting + void registerTransitionCallback(OneHandedTransitionCallback callback) { + mDisplayAreaOrganizer.registerTransitionCallback(callback); + } + + private void registerGestureCallback(OneHandedGestureEventCallback callback) { + mGestureHandler.setGestureEventListener(callback); + } + + private void setupCallback() { + mTouchHandler.registerTouchEventListener(() -> + stopOneHanded(OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_OVERSPACE_OUT)); + mDisplayAreaOrganizer.registerTransitionCallback(mTouchHandler); + mDisplayAreaOrganizer.registerTransitionCallback(mGestureHandler); + mDisplayAreaOrganizer.registerTransitionCallback(mTutorialHandler); + mDisplayAreaOrganizer.registerTransitionCallback(mBackgroundPanelOrganizer); + if (mTaskChangeToExit) { + mTaskStackListener.addListener(mTaskStackListenerCallback); + } + } + + 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 ContentObserver getObserver(Runnable onChangeRunnable) { + return new ContentObserver(mMainHandler) { + @Override + public void onChange(boolean selfChange) { + onChangeRunnable.run(); + } + }; + } + + private void onEnabledSettingChanged() { + final boolean enabled = OneHandedSettingsUtil.getSettingsOneHandedModeEnabled( + mContext.getContentResolver()); + mOneHandedUiEventLogger.writeEvent(enabled + ? OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_ENABLED_ON + : OneHandedUiEventLogger.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 void onTimeoutSettingChanged() { + final int newTimeout = OneHandedSettingsUtil.getSettingsOneHandedModeTimeout( + mContext.getContentResolver()); + int metricsId = OneHandedUiEventLogger.OneHandedSettingsTogglesEvent.INVALID.getId(); + switch (newTimeout) { + case OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER: + metricsId = OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_NEVER; + break; + case OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS: + metricsId = OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_4; + break; + case OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS: + metricsId = OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_8; + break; + case OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_LONG_IN_SECONDS: + metricsId = OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_12; + break; + default: + // do nothing + break; + } + mOneHandedUiEventLogger.writeEvent(metricsId); + + if (mTimeoutHandler != null) { + mTimeoutHandler.setTimeout(newTimeout); + } + } + + private void onTaskChangeExitSettingChanged() { + final boolean enabled = OneHandedSettingsUtil.getSettingsTapsAppToExit( + mContext.getContentResolver()); + mOneHandedUiEventLogger.writeEvent(enabled + ? OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_ON + : OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_OFF); + + setTaskChangeToExit(enabled); + } + + private void onSwipeToNotificationEnabledSettingChanged() { + 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())); + } + + private void setupTimeoutListener() { + mTimeoutHandler.registerTimeoutListener(timeoutTime -> stopOneHanded( + OneHandedUiEventLogger.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(); + mBackgroundPanelOrganizer.unregisterOrganizer(); + if (mIsOneHandedEnabled) { + mDisplayAreaOrganizer.registerOrganizer( + OneHandedDisplayAreaOrganizer.FEATURE_ONE_HANDED); + mBackgroundPanelOrganizer.registerOrganizer( + OneHandedBackgroundPanelOrganizer.FEATURE_ONE_HANDED_BACKGROUND_PANEL); + } + 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(); + } + } + + private 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); + } + } + } + + @ExternalThread + private class OneHandedImpl implements OneHanded { + @Override + public boolean isOneHandedEnabled() { + // This is volatile so return directly + return mIsOneHandedEnabled; + } + + @Override + public boolean isSwipeToNotificationEnabled() { + // This is volatile so return directly + return mIsSwipeToNotificationEnabled; + } + + @Override + public void startOneHanded() { + mMainExecutor.execute(() -> { + OneHandedController.this.startOneHanded(); + }); + } + + @Override + public void stopOneHanded() { + mMainExecutor.execute(() -> { + OneHandedController.this.stopOneHanded(); + }); + } + + @Override + public void stopOneHanded(int event) { + mMainExecutor.execute(() -> { + OneHandedController.this.stopOneHanded(event); + }); + } + + @Override + public void setThreeButtonModeEnabled(boolean enabled) { + mMainExecutor.execute(() -> { + OneHandedController.this.setThreeButtonModeEnabled(enabled); + }); + } + + @Override + public void registerTransitionCallback(OneHandedTransitionCallback callback) { + mMainExecutor.execute(() -> { + OneHandedController.this.registerTransitionCallback(callback); + }); + } + + @Override + public void registerGestureCallback(OneHandedGestureEventCallback callback) { + mMainExecutor.execute(() -> { + OneHandedController.this.registerGestureCallback(callback); + }); + } + + @Override + public void dump(@NonNull PrintWriter pw) { + mMainExecutor.execute(() -> { + OneHandedController.this.dump(pw); + }); + } + } +} 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..04d1264bdd9d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java @@ -0,0 +1,322 @@ +/* + * 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.SystemProperties; +import android.util.ArrayMap; +import android.view.SurfaceControl; +import android.window.DisplayAreaAppearedInfo; +import android.window.DisplayAreaInfo; +import android.window.DisplayAreaOrganizer; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.wm.shell.R; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * 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"; + + private final Rect mLastVisualDisplayBounds = new Rect(); + private final Rect mDefaultDisplayBounds = new Rect(); + + private boolean mIsInOneHanded; + private int mEnterExitAnimationDurationMs; + + @VisibleForTesting + ArrayMap<WindowContainerToken, SurfaceControl> mDisplayAreaTokenMap = new ArrayMap(); + private DisplayController mDisplayController; + private OneHandedAnimationController mAnimationController; + private OneHandedSurfaceTransactionHelper.SurfaceControlTransactionFactory + mSurfaceControlTransactionFactory; + private OneHandedTutorialHandler mTutorialHandler; + private List<OneHandedTransitionCallback> mTransitionCallbacks = new ArrayList<>(); + private OneHandedBackgroundPanelOrganizer mBackgroundPanelOrganizer; + + @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.getToken()); + if (mAnimationController.isAnimatorsConsumed()) { + resetWindowsOffsetInternal(animator.getTransitionDirection()); + finishOffset(animator.getDestinationOffset(), + animator.getTransitionDirection()); + } + } + + @Override + public void onOneHandedAnimationCancel( + OneHandedAnimationController.OneHandedTransitionAnimator animator) { + mAnimationController.removeAnimator(animator.getToken()); + if (mAnimationController.isAnimatorsConsumed()) { + resetWindowsOffsetInternal(animator.getTransitionDirection()); + finishOffset(animator.getDestinationOffset(), + animator.getTransitionDirection()); + } + } + }; + + /** + * Constructor of OneHandedDisplayAreaOrganizer + */ + public OneHandedDisplayAreaOrganizer(Context context, + DisplayController displayController, + OneHandedAnimationController animationController, + OneHandedTutorialHandler tutorialHandler, + OneHandedBackgroundPanelOrganizer oneHandedBackgroundGradientOrganizer, + ShellExecutor mainExecutor) { + super(mainExecutor); + mAnimationController = animationController; + mDisplayController = displayController; + mLastVisualDisplayBounds.set(getDisplayBounds()); + final int animationDurationConfig = context.getResources().getInteger( + R.integer.config_one_handed_translate_animation_duration); + mEnterExitAnimationDurationMs = + SystemProperties.getInt(ONE_HANDED_MODE_TRANSLATE_ANIMATION_DURATION, + animationDurationConfig); + mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; + mTutorialHandler = tutorialHandler; + mBackgroundPanelOrganizer = oneHandedBackgroundGradientOrganizer; + } + + @Override + public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo, + @NonNull SurfaceControl leash) { + mDisplayAreaTokenMap.put(displayAreaInfo.token, leash); + } + + @Override + public void onDisplayAreaVanished(@NonNull DisplayAreaInfo displayAreaInfo) { + mDisplayAreaTokenMap.remove(displayAreaInfo.token); + } + + @Override + public List<DisplayAreaAppearedInfo> registerOrganizer(int displayAreaFeature) { + final List<DisplayAreaAppearedInfo> displayAreaInfos = + super.registerOrganizer(displayAreaFeature); + for (int i = 0; i < displayAreaInfos.size(); i++) { + final DisplayAreaAppearedInfo info = displayAreaInfos.get(i); + onDisplayAreaAppeared(info.getDisplayAreaInfo(), info.getLeash()); + } + mDefaultDisplayBounds.set(getDisplayBounds()); + return displayAreaInfos; + } + + @Override + public void unregisterOrganizer() { + super.unregisterOrganizer(); + 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) { + resetWindowsOffset(wct); + mDefaultDisplayBounds.set(newBounds); + mLastVisualDisplayBounds.set(newBounds); + finishOffset(0, TRANSITION_DIRECTION_EXIT); + } + } + + /** + * 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) { + final Rect toBounds = new Rect(mDefaultDisplayBounds.left, + mDefaultDisplayBounds.top + yOffset, + mDefaultDisplayBounds.right, + mDefaultDisplayBounds.bottom + yOffset); + final Rect fromBounds = getLastVisualDisplayBounds() != null + ? getLastVisualDisplayBounds() + : mDefaultDisplayBounds; + final int direction = yOffset > 0 + ? TRANSITION_DIRECTION_TRIGGER + : TRANSITION_DIRECTION_EXIT; + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mDisplayAreaTokenMap.forEach( + (token, leash) -> { + animateWindows(token, leash, fromBounds, toBounds, direction, + mEnterExitAnimationDurationMs); + wct.setBounds(token, toBounds); + }); + applyTransaction(wct); + } + + private void resetWindowsOffsetInternal( + @OneHandedAnimationController.TransitionDirection int td) { + if (td == TRANSITION_DIRECTION_TRIGGER) { + return; + } + final WindowContainerTransaction wct = new WindowContainerTransaction(); + resetWindowsOffset(wct); + applyTransaction(wct); + } + + private void resetWindowsOffset(WindowContainerTransaction wct) { + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); + mDisplayAreaTokenMap.forEach( + (token, leash) -> { + final OneHandedAnimationController.OneHandedTransitionAnimator animator = + mAnimationController.getAnimatorMap().remove(token); + 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(token, null/* reset */); + } + }); + tx.apply(); + } + + private void animateWindows(WindowContainerToken token, SurfaceControl leash, Rect fromBounds, + Rect toBounds, @OneHandedAnimationController.TransitionDirection int direction, + int durationMs) { + final OneHandedAnimationController.OneHandedTransitionAnimator animator = + mAnimationController.getAnimator(token, leash, fromBounds, toBounds); + if (animator != null) { + animator.setTransitionDirection(direction) + .addOneHandedAnimationCallback(mOneHandedAnimationCallback) + .addOneHandedAnimationCallback(mTutorialHandler.getAnimationCallback()) + .addOneHandedAnimationCallback( + mBackgroundPanelOrganizer.getOneHandedAnimationCallback()) + .setDuration(durationMs) + .start(); + } + } + + @VisibleForTesting + void finishOffset(int offset, + @OneHandedAnimationController.TransitionDirection int direction) { + // 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); + } + + /** + * Register transition callback + */ + public void registerTransitionCallback(OneHandedTransitionCallback callback) { + mTransitionCallbacks.add(callback); + } + + void dump(@NonNull PrintWriter pw) { + final String innerPrefix = " "; + pw.println(TAG + "states: "); + pw.print(innerPrefix + "mIsInOneHanded="); + pw.println(mIsInOneHanded); + pw.print(innerPrefix + "mDisplayAreaTokenMap="); + pw.println(mDisplayAreaTokenMap); + 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/OneHandedGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedGestureHandler.java new file mode 100644 index 000000000000..49b7e050c48b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedGestureHandler.java @@ -0,0 +1,287 @@ +/* + * 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; +import com.android.wm.shell.common.ShellExecutor; + +/** + * 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 final DisplayController mDisplayController; + private final ShellExecutor mMainExecutor; + @VisibleForTesting + @Nullable + OneHandedGestureEventCallback mGestureEventCallback; + private Rect mGestureRegion = new Rect(); + private boolean mIsStopGesture; + + /** + * 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, + ShellExecutor mainExecutor) { + mDisplayController = displayController; + mMainExecutor = mainExecutor; + displayController.addDisplayChangingController(this); + mNavGestureHeight = context.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.navigation_bar_gesture_larger_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) { + mIsStopGesture = true; + } + } + break; + case MotionEvent.ACTION_UP: + if (mLastPos.y >= mDownPos.y && mPassedSlop) { + mGestureEventCallback.onStart(); + } else if (mIsStopGesture) { + mGestureEventCallback.onStop(); + } + clearState(); + break; + case MotionEvent.ACTION_CANCEL: + clearState(); + break; + default: + break; + } + } + } + + private void clearState() { + mPassedSlop = false; + mIsStopGesture = false; + } + + 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); + try { + mMainExecutor.executeBlocking(() -> { + mInputEventReceiver = new EventReceiver( + mInputMonitor.getInputChannel(), Looper.myLooper()); + }); + } catch (InterruptedException e) { + throw new RuntimeException("Failed to create input event receiver", e); + } + } + } + + 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; + } + + // TODO: Use BatchedInputEventReceiver + 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..f8217c64e53d --- /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, 1) == 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/OneHandedTimeoutHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandler.java new file mode 100644 index 000000000000..4a98941b7410 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandler.java @@ -0,0 +1,132 @@ +/* + * 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 androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.android.wm.shell.common.ShellExecutor; + +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 final ShellExecutor mMainExecutor; + + // Default timeout is ONE_HANDED_TIMEOUT_MEDIUM + private @OneHandedSettingsUtil.OneHandedTimeout int mTimeout = + ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS; + private long mTimeoutMs = TimeUnit.SECONDS.toMillis(mTimeout); + private final Runnable mTimeoutRunnable = this::onStop; + private List<TimeoutListener> mListeners = new ArrayList<>(); + + /** + * Get the current config of timeout + * + * @return timeout of current config + */ + public @OneHandedSettingsUtil.OneHandedTimeout int getTimeout() { + return mTimeout; + } + + /** + * 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); + } + + public OneHandedTimeoutHandler(ShellExecutor mainExecutor) { + mMainExecutor = mainExecutor; + } + + /** + * Set the specific timeout of {@link OneHandedSettingsUtil.OneHandedTimeout} + */ + public void setTimeout(@OneHandedSettingsUtil.OneHandedTimeout int timeout) { + mTimeout = timeout; + mTimeoutMs = TimeUnit.SECONDS.toMillis(mTimeout); + resetTimer(); + } + + /** + * Reset the timer when one handed trigger or user is operating in some conditions + */ + public void removeTimer() { + mMainExecutor.removeCallbacks(mTimeoutRunnable); + } + + /** + * Reset the timer when one handed trigger or user is operating in some conditions + */ + public void resetTimer() { + removeTimer(); + if (mTimeout == OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER) { + return; + } + if (mTimeout != OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER) { + mMainExecutor.executeDelayed(mTimeoutRunnable, mTimeoutMs); + } + } + + /** + * Register timeout listener to receive time out events + * + * @param listener the listener be sent events when times up + */ + public void registerTimeoutListener(TimeoutListener listener) { + mListeners.add(listener); + } + + @VisibleForTesting + boolean hasScheduledTimeout() { + return mMainExecutor.hasCallback(mTimeoutRunnable); + } + + private void onStop() { + for (int i = mListeners.size() - 1; i >= 0; i--) { + final TimeoutListener listener = mListeners.get(i); + listener.onTimeout(mTimeout); + } + } + + void dump(@NonNull PrintWriter pw) { + final String innerPrefix = " "; + pw.println(TAG + "states: "); + pw.print(innerPrefix + "sTimeout="); + pw.println(mTimeout); + pw.print(innerPrefix + "sListeners="); + pw.println(mListeners); + } + +} 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..c7a49ff01d15 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTouchHandler.java @@ -0,0 +1,185 @@ +/* + * 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 com.android.wm.shell.common.ShellExecutor; + +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 final OneHandedTimeoutHandler mTimeoutHandler; + private final ShellExecutor mMainExecutor; + + @VisibleForTesting + InputMonitor mInputMonitor; + @VisibleForTesting + InputEventReceiver mInputEventReceiver; + @VisibleForTesting + OneHandedTouchEventCallback mTouchEventCallback; + + private boolean mIsEnabled; + private boolean mIsOnStopTransitioning; + private boolean mIsInOutsideRegion; + + public OneHandedTouchHandler(OneHandedTimeoutHandler timeoutHandler, + ShellExecutor mainExecutor) { + mTimeoutHandler = timeoutHandler; + mMainExecutor = mainExecutor; + 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); + try { + mMainExecutor.executeBlocking(() -> { + mInputEventReceiver = new EventReceiver( + mInputMonitor.getInputChannel(), Looper.myLooper()); + }); + } catch (InterruptedException e) { + throw new RuntimeException("Failed to create input event receiver", e); + } + } + } + + @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); + } + + // TODO: Use BatchedInputEventReceiver + 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..492130bebb30 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTutorialHandler.java @@ -0,0 +1,245 @@ +/* + * 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.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 com.android.wm.shell.common.ShellExecutor; + +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 ContentResolver mContentResolver; + private boolean mCanShowTutorial; + private String mStartOneHandedDescription; + private String mStopOneHandedDescription; + + private enum ONE_HANDED_TRIGGER_STATE { + UNSET, ENTERING, EXITING + } + /** + * Current One-Handed trigger state. + * Note: This is a dynamic state, whenever last state has been confirmed + * (i.e. onStartFinished() or onStopFinished()), the state should be set "UNSET" at final. + */ + private ONE_HANDED_TRIGGER_STATE mTriggerState = ONE_HANDED_TRIGGER_STATE.UNSET; + + /** + * 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) { + onAnimationUpdate(offset); + } + + @Override + public void onOneHandedAnimationStart( + OneHandedAnimationController.OneHandedTransitionAnimator animator) { + final Rect startValue = (Rect) animator.getStartValue(); + if (mTriggerState == ONE_HANDED_TRIGGER_STATE.UNSET) { + mTriggerState = (startValue.top == 0) + ? ONE_HANDED_TRIGGER_STATE.ENTERING : ONE_HANDED_TRIGGER_STATE.EXITING; + if (mCanShowTutorial && mTriggerState == ONE_HANDED_TRIGGER_STATE.ENTERING) { + createTutorialTarget(); + } + } + } + }; + + public OneHandedTutorialHandler(Context context, ShellExecutor mainExecutor) { + context.getDisplay().getRealSize(mDisplaySize); + mPackageName = context.getPackageName(); + mContentResolver = context.getContentResolver(); + mWindowManager = context.getSystemService(WindowManager.class); + mAccessibilityManager = (AccessibilityManager) + context.getSystemService(Context.ACCESSIBILITY_SERVICE); + + mStartOneHandedDescription = context.getResources().getString( + R.string.accessibility_action_start_one_handed); + mStopOneHandedDescription = context.getResources().getString( + R.string.accessibility_action_stop_one_handed); + mCanShowTutorial = (Settings.Secure.getInt(mContentResolver, + Settings.Secure.ONE_HANDED_TUTORIAL_SHOW_COUNT, 0) >= MAX_TUTORIAL_SHOW_COUNT) + ? false : true; + final float offsetPercentageConfig = context.getResources().getFraction( + R.fraction.config_one_handed_offset, 1, 1); + final int sysPropPercentageConfig = SystemProperties.getInt( + ONE_HANDED_MODE_OFFSET_PERCENTAGE, Math.round(offsetPercentageConfig * 100.0f)); + mTutorialAreaHeight = Math.round(mDisplaySize.y * (sysPropPercentageConfig / 100.0f)); + + mainExecutor.execute(() -> { + mTutorialView = LayoutInflater.from(context).inflate(R.layout.one_handed_tutorial, + null); + mTargetViewContainer = new FrameLayout(context); + mTargetViewContainer.setClipChildren(false); + mTargetViewContainer.addView(mTutorialView); + }); + } + + @Override + public void onStartFinished(Rect bounds) { + updateFinished(View.VISIBLE, 0f); + updateTutorialCount(); + announcementForScreenReader(true); + mTriggerState = ONE_HANDED_TRIGGER_STATE.UNSET; + } + + @Override + public void onStopFinished(Rect bounds) { + updateFinished(View.INVISIBLE, -mTargetViewContainer.getHeight()); + announcementForScreenReader(false); + removeTutorialFromWindowManager(); + mTriggerState = ONE_HANDED_TRIGGER_STATE.UNSET; + } + + 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 createTutorialTarget() { + if (!mTargetViewContainer.isAttachedToWindow()) { + 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()); + } + } + } + + private void removeTutorialFromWindowManager() { + if (mTargetViewContainer.isAttachedToWindow()) { + mWindowManager.removeViewImmediate(mTargetViewContainer); + } + } + + 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) { + // Since canSHowTutorial() will be called in onAnimationUpdate() and we still need to + // hide Tutorial text in the period of continuously onAnimationUpdate() API call, + // so we have to hide mTargetViewContainer here. + 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/onehanded/OneHandedUiEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedUiEventLogger.java new file mode 100644 index 000000000000..38ffb07aa5a4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedUiEventLogger.java @@ -0,0 +1,255 @@ +/* + * 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; + +/** + * Helper class that ends OneHanded mode log to UiEvent, see also go/uievent + */ +public class OneHandedUiEventLogger { + private static final String TAG = "OneHandedUiEventLogger"; + private final UiEventLogger mUiEventLogger; + + /** + * 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" + }; + + public OneHandedUiEventLogger(UiEventLogger uiEventLogger) { + mUiEventLogger = uiEventLogger; + } + + /** + * 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; + } + + @Override + 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; + } + + @Override + 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 void writeEvent(int tag) { + logEvent(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. + */ + private void logEvent(int event) { + switch (event) { + case EVENT_ONE_HANDED_TRIGGER_GESTURE_IN: + mUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_GESTURE_IN); + break; + case EVENT_ONE_HANDED_TRIGGER_GESTURE_OUT: + mUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_GESTURE_OUT); + break; + case EVENT_ONE_HANDED_TRIGGER_OVERSPACE_OUT: + mUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_OVERSPACE_OUT); + break; + case EVENT_ONE_HANDED_TRIGGER_POP_IME_OUT: + mUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_POP_IME_OUT); + break; + case EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT: + mUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_ROTATION_OUT); + break; + case EVENT_ONE_HANDED_TRIGGER_APP_TAPS_OUT: + mUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_APP_TAPS_OUT); + break; + case EVENT_ONE_HANDED_TRIGGER_TIMEOUT_OUT: + mUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_TIMEOUT_OUT); + break; + case EVENT_ONE_HANDED_TRIGGER_SCREEN_OFF_OUT: + mUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_SCREEN_OFF_OUT); + break; + case EVENT_ONE_HANDED_SETTINGS_ENABLED_ON: + mUiEventLogger.log(OneHandedSettingsTogglesEvent + .ONE_HANDED_SETTINGS_TOGGLES_ENABLED_ON); + break; + case EVENT_ONE_HANDED_SETTINGS_ENABLED_OFF: + mUiEventLogger.log(OneHandedSettingsTogglesEvent + .ONE_HANDED_SETTINGS_TOGGLES_ENABLED_OFF); + break; + case EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_ON: + mUiEventLogger.log(OneHandedSettingsTogglesEvent + .ONE_HANDED_SETTINGS_TOGGLES_APP_TAPS_EXIT_ON); + break; + case EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_OFF: + mUiEventLogger.log(OneHandedSettingsTogglesEvent + .ONE_HANDED_SETTINGS_TOGGLES_APP_TAPS_EXIT_OFF); + break; + case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_ON: + mUiEventLogger.log(OneHandedSettingsTogglesEvent + .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_EXIT_ON); + break; + case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_OFF: + mUiEventLogger.log(OneHandedSettingsTogglesEvent + .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_EXIT_OFF); + break; + case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_NEVER: + mUiEventLogger.log(OneHandedSettingsTogglesEvent + .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_NEVER); + break; + case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_4: + mUiEventLogger.log(OneHandedSettingsTogglesEvent + .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_4); + break; + case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_8: + mUiEventLogger.log(OneHandedSettingsTogglesEvent + .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_8); + break; + case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_12: + mUiEventLogger.log(OneHandedSettingsTogglesEvent + .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_12); + break; + default: + // Do nothing + break; + } + } +} 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..8f8ec475a85c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java @@ -0,0 +1,146 @@ +/* + * 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.os.RemoteException; +import android.view.IPinnedStackListener; +import android.view.WindowManagerGlobal; + +import androidx.annotation.BinderThread; + +import com.android.wm.shell.common.ShellExecutor; + +import java.util.ArrayList; + +/** + * 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 { + + private final IPinnedStackListener mListenerImpl = new PinnedStackListenerImpl(); + private final ShellExecutor mMainExecutor; + private final ArrayList<PinnedStackListener> mListeners = new ArrayList<>(); + + public PinnedStackListenerForwarder(ShellExecutor mainExecutor) { + mMainExecutor = mainExecutor; + } + + /** 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); + } + + public void register(int displayId) throws RemoteException { + WindowManagerGlobal.getWindowManagerService().registerPinnedStackListener( + displayId, mListenerImpl); + } + + private void onMovementBoundsChanged(boolean fromImeAdjustment) { + for (PinnedStackListener listener : mListeners) { + listener.onMovementBoundsChanged(fromImeAdjustment); + } + } + + private void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { + for (PinnedStackListener listener : mListeners) { + listener.onImeVisibilityChanged(imeVisible, imeHeight); + } + } + + private void onActionsChanged(ParceledListSlice<RemoteAction> actions) { + for (PinnedStackListener listener : mListeners) { + listener.onActionsChanged(actions); + } + } + + private void onActivityHidden(ComponentName componentName) { + for (PinnedStackListener listener : mListeners) { + listener.onActivityHidden(componentName); + } + } + + private void onAspectRatioChanged(float aspectRatio) { + for (PinnedStackListener listener : mListeners) { + listener.onAspectRatioChanged(aspectRatio); + } + } + + @BinderThread + private class PinnedStackListenerImpl extends IPinnedStackListener.Stub { + @Override + public void onMovementBoundsChanged(boolean fromImeAdjustment) { + mMainExecutor.execute(() -> { + PinnedStackListenerForwarder.this.onMovementBoundsChanged(fromImeAdjustment); + }); + } + + @Override + public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { + mMainExecutor.execute(() -> { + PinnedStackListenerForwarder.this.onImeVisibilityChanged(imeVisible, imeHeight); + }); + } + + @Override + public void onActionsChanged(ParceledListSlice<RemoteAction> actions) { + mMainExecutor.execute(() -> { + PinnedStackListenerForwarder.this.onActionsChanged(actions); + }); + } + + @Override + public void onActivityHidden(ComponentName componentName) { + mMainExecutor.execute(() -> { + PinnedStackListenerForwarder.this.onActivityHidden(componentName); + }); + } + + @Override + public void onAspectRatioChanged(float aspectRatio) { + mMainExecutor.execute(() -> { + PinnedStackListenerForwarder.this.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 onMovementBoundsChanged(boolean fromImeAdjustment) {} + + public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {} + + public void onActionsChanged(ParceledListSlice<RemoteAction> actions) {} + + public void onActivityHidden(ComponentName componentName) {} + + 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..d14c3e3c0dd4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java @@ -0,0 +1,149 @@ +/* + * 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.PictureInPictureParams; +import android.content.ComponentName; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.graphics.Rect; + +import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.pip.phone.PipTouchHandler; + +import java.io.PrintWriter; +import java.util.function.Consumer; + +/** + * Interface to engage picture in picture feature. + */ +@ExternalThread +public interface Pip { + /** + * Expand PIP, it's possible that specific request to activate the window via Alt-tab. + */ + default void expandPip() { + } + + /** + * Hides the PIP menu. + */ + default void hidePipMenu(Runnable onStartCallback, Runnable onEndCallback) {} + + /** + * Called when configuration is changed. + */ + default void onConfigurationChanged(Configuration newConfig) { + } + + /** + * Called when display size or font size of settings changed + */ + default void onDensityOrFontScaleChanged() { + } + + /** + * Called when overlay package change invoked. + */ + default void onOverlayChanged() { + } + + /** + * 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) { + } + + /** + * Registers the session listener for the current user. + */ + default void registerSessionListenerForCurrentUser() { + } + + /** + * 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() {} + + /** + * 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; + } + + /** + * Called by NavigationBar in order to listen in for PiP bounds change. This is mostly used + * for times where the PiP bounds could conflict with SystemUI elements, such as a stashed + * PiP and the Back-from-Edge gesture. + */ + default void setPipExclusionBoundsChangeListener(Consumer<Rect> listener) { } + + /** + * Dump the current state and information if need. + * + * @param pw The stream to dump information to. + */ + default void dump(PrintWriter pw) { + } +} 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..90992fb92324 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -0,0 +1,508 @@ +/* + * 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.Choreographer; +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; + public static final int TRANSITION_DIRECTION_SNAP_AFTER_RESIZE = 6; + public static final int TRANSITION_DIRECTION_USER_RESIZE = 7; + public static final int TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND = 8; + + @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, + TRANSITION_DIRECTION_SNAP_AFTER_RESIZE, + TRANSITION_DIRECTION_USER_RESIZE, + TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND + }) + @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") + /** + * Construct and return an animator that animates from the {@param startBounds} to the + * {@param endBounds} with the given {@param direction}. If {@param direction} is type + * {@link ANIM_TYPE_BOUNDS}, then {@param sourceHintRect} will be used to animate + * in a better, more smooth manner. If the original bound was rotated and a reset needs to + * happen, pass in {@param startingAngle}. + * + * In the case where one wants to start animation during an intermediate animation (for example, + * if the user is currently doing a pinch-resize, and upon letting go now PiP needs to animate + * to the correct snap fraction region), then provide the base bounds, which is current PiP + * leash bounds before transformation/any animation. This is so when we try to construct + * the different transformation matrices for the animation, we are constructing this based off + * the PiP original bounds, rather than the {@param startBounds}, which is post-transformed. + */ + @VisibleForTesting + public PipTransitionAnimator getAnimator(SurfaceControl leash, Rect baseBounds, + Rect startBounds, Rect endBounds, Rect sourceHintRect, + @PipAnimationController.TransitionDirection int direction, float startingAngle) { + if (mCurrentAnimator == null) { + mCurrentAnimator = setupPipTransitionAnimator( + PipTransitionAnimator.ofBounds(leash, startBounds, startBounds, endBounds, + sourceHintRect, direction, 0 /* startingAngle */)); + } 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, baseBounds, startBounds, endBounds, + sourceHintRect, direction, startingAngle)); + } + 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(); + + private T mBaseValue; + protected T mCurrentValue; + protected T mStartValue; + private T mEndValue; + private float mStartingAngle; + private PipAnimationCallback mPipAnimationCallback; + private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory + mSurfaceControlTransactionFactory; + private PipSurfaceTransactionHelper mSurfaceTransactionHelper; + private @TransitionDirection int mTransitionDirection; + + private PipTransitionAnimator(SurfaceControl leash, @AnimationType int animationType, + Rect destinationBounds, T baseValue, T startValue, T endValue, + float startingAngle) { + mLeash = leash; + mAnimationType = animationType; + mDestinationBounds.set(destinationBounds); + mBaseValue = baseValue; + mStartValue = startValue; + mEndValue = endValue; + mStartingAngle = startingAngle; + 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; + } + + T getBaseValue() { + return mBaseValue; + } + + @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; + } + + /** + * @return {@link SurfaceControl.Transaction} instance with vsync-id. + */ + protected SurfaceControl.Transaction newSurfaceControlTransaction() { + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); + tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId()); + return tx; + } + + @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, startValue, endValue, 0) { + @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 baseValue, Rect startValue, Rect endValue, Rect sourceHintRect, + @PipAnimationController.TransitionDirection int direction, float startingAngle) { + // 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(baseValue); + } + + 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(baseValue), new Rect(startValue), new Rect(endValue), + startingAngle) { + 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 base = getBaseValue(); + final Rect start = getStartValue(); + final Rect end = getEndValue(); + Rect bounds = mRectEvaluator.evaluate(fraction, start, end); + float angle = (1.0f - fraction) * startingAngle; + setCurrentValue(bounds); + if (inScaleTransition() || sourceHintRect == null) { + + if (isOutPipDirection(direction)) { + getSurfaceTransactionHelper().scale(tx, leash, end, bounds); + } else { + getSurfaceTransactionHelper().scale(tx, leash, base, bounds, angle); + } + } 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()); + // TODO(b/178632364): this is a work around for the black background when + // entering PiP in buttion navigation mode. + if (isInPipDirection(direction)) { + tx.setWindowCrop(leash, getStartValue()); + } + 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/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java new file mode 100644 index 000000000000..a8961ea3d8a8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java @@ -0,0 +1,410 @@ +/* + * 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.util.TypedValue.COMPLEX_UNIT_DIP; + +import android.annotation.NonNull; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.util.DisplayMetrics; +import android.util.Size; +import android.util.TypedValue; +import android.view.DisplayInfo; +import android.view.Gravity; + +import com.android.wm.shell.common.DisplayLayout; + +import java.io.PrintWriter; + +/** + * Calculates the default, normal, entry, inset and movement bounds of the PIP. + */ +public class PipBoundsAlgorithm { + + private static final String TAG = PipBoundsAlgorithm.class.getSimpleName(); + private static final float INVALID_SNAP_FRACTION = -1f; + + private final @NonNull PipBoundsState mPipBoundsState; + private final PipSnapAlgorithm mSnapAlgorithm; + + private float mDefaultSizePercent; + private float mMinAspectRatioForMinSize; + private float mMaxAspectRatioForMinSize; + private float mDefaultAspectRatio; + private float mMinAspectRatio; + private float mMaxAspectRatio; + private int mDefaultStackGravity; + private int mDefaultMinSize; + private Point mScreenEdgeInsets; + + public PipBoundsAlgorithm(Context context, @NonNull PipBoundsState pipBoundsState) { + mPipBoundsState = pipBoundsState; + mSnapAlgorithm = new PipSnapAlgorithm(); + 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); + mPipBoundsState.setMinEdgeSize(mDefaultMinSize); + } + + /** + * 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); + 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); + mDefaultSizePercent = res.getFloat( + com.android.internal.R.dimen.config_pictureInPictureDefaultSizePercent); + mMaxAspectRatioForMinSize = res.getFloat( + com.android.internal.R.dimen.config_pictureInPictureAspectRatioLimitForMinSize); + mMinAspectRatioForMinSize = 1f / mMaxAspectRatioForMinSize; + } + + /** + * The {@link PipSnapAlgorithm} is couple on display bounds + * @return {@link PipSnapAlgorithm}. + */ + public PipSnapAlgorithm getSnapAlgorithm() { + return mSnapAlgorithm; + } + + /** Responds to configuration change. */ + public void onConfigurationChanged(Context context) { + reloadResources(context); + } + + /** Returns the normal bounds (i.e. the default entry bounds). */ + public Rect getNormalBounds() { + // The normal bounds are the default bounds adjusted to the current aspect ratio. + return transformBoundsToAspectRatioIfValid(getDefaultBounds(), + mPipBoundsState.getAspectRatio(), false /* useCurrentMinEdgeSize */, + false /* useCurrentSize */); + } + + /** Returns the default bounds. */ + public Rect getDefaultBounds() { + return getDefaultBounds(INVALID_SNAP_FRACTION, null /* size */); + } + + /** Returns the destination bounds to place the PIP window on entry. */ + public Rect getEntryDestinationBounds() { + final PipBoundsState.PipReentryState reentryState = mPipBoundsState.getReentryState(); + + final Rect destinationBounds = reentryState != null + ? getDefaultBounds(reentryState.getSnapFraction(), reentryState.getSize()) + : getDefaultBounds(); + + final boolean useCurrentSize = reentryState != null && reentryState.getSize() != null; + return transformBoundsToAspectRatioIfValid(destinationBounds, + mPipBoundsState.getAspectRatio(), false /* useCurrentMinEdgeSize */, + useCurrentSize); + } + + /** Returns the current bounds adjusted to the new aspect ratio, if valid. */ + public Rect getAdjustedDestinationBounds(Rect currentBounds, float newAspectRatio) { + return transformBoundsToAspectRatioIfValid(currentBounds, newAspectRatio, + true /* useCurrentMinEdgeSize */, false /* useCurrentSize */); + } + + public float getDefaultAspectRatio() { + return mDefaultAspectRatio; + } + + /** + * @return whether the given {@param aspectRatio} is valid. + */ + private boolean isValidPictureInPictureAspectRatio(float aspectRatio) { + return Float.compare(mMinAspectRatio, aspectRatio) <= 0 + && Float.compare(aspectRatio, mMaxAspectRatio) <= 0; + } + + private Rect transformBoundsToAspectRatioIfValid(Rect bounds, float aspectRatio, + boolean useCurrentMinEdgeSize, boolean useCurrentSize) { + final Rect destinationBounds = new Rect(bounds); + if (isValidPictureInPictureAspectRatio(aspectRatio)) { + transformBoundsToAspectRatio(destinationBounds, aspectRatio, + useCurrentMinEdgeSize, useCurrentSize); + } + return destinationBounds; + } + + /** + * Set the current bounds (or the default bounds if there are no current bounds) with the + * specified aspect ratio. + */ + public 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), mPipBoundsState.getStashedState()); + + final Size overrideMinSize = mPipBoundsState.getOverrideMinSize(); + final Size size; + if (useCurrentMinEdgeSize || useCurrentSize) { + // The default minimum edge size, or the override min edge size if set. + final int defaultMinEdgeSize = overrideMinSize == null ? mDefaultMinSize + : mPipBoundsState.getOverrideMinEdgeSize(); + final int minEdgeSize = useCurrentMinEdgeSize ? mPipBoundsState.getMinEdgeSize() + : defaultMinEdgeSize; + // Use the existing size but adjusted to the aspect ratio and min edge size. + size = getSizeForAspectRatio( + new Size(stackBounds.width(), stackBounds.height()), aspectRatio, minEdgeSize); + } else { + if (overrideMinSize != null) { + // The override minimal size is set, use that as the default size making sure it's + // adjusted to the aspect ratio. + size = adjustSizeToAspectRatio(overrideMinSize, aspectRatio); + } else { + // Calculate the default size using the display size and default min edge size. + final DisplayLayout displayLayout = mPipBoundsState.getDisplayLayout(); + size = getSizeForAspectRatio(aspectRatio, mDefaultMinSize, + displayLayout.width(), displayLayout.height()); + } + } + + 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()); + mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction); + } + + /** Adjusts the given size to conform to the given aspect ratio. */ + private Size adjustSizeToAspectRatio(@NonNull Size size, float aspectRatio) { + final float sizeAspectRatio = size.getWidth() / (float) size.getHeight(); + if (sizeAspectRatio > aspectRatio) { + // Size is wider, fix the width and increase the height + return new Size(size.getWidth(), (int) (size.getWidth() / aspectRatio)); + } else { + // Size is taller, fix the height and adjust the width. + return new Size((int) (size.getHeight() * aspectRatio), size.getHeight()); + } + } + + /** + * @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) { + // The default bounds are the given size positioned at the given snap fraction. + defaultBounds.set(0, 0, size.getWidth(), size.getHeight()); + final Rect movementBounds = getMovementBounds(defaultBounds); + mSnapAlgorithm.applySnapFraction(defaultBounds, movementBounds, snapFraction); + return defaultBounds; + } + + // Calculate the default size. + final Size defaultSize; + final Rect insetBounds = new Rect(); + getInsetBounds(insetBounds); + final DisplayLayout displayLayout = mPipBoundsState.getDisplayLayout(); + final Size overrideMinSize = mPipBoundsState.getOverrideMinSize(); + if (overrideMinSize != null) { + // The override minimal size is set, use that as the default size making sure it's + // adjusted to the aspect ratio. + defaultSize = adjustSizeToAspectRatio(overrideMinSize, mDefaultAspectRatio); + } else { + // Calculate the default size using the display size and default min edge size. + defaultSize = getSizeForAspectRatio(mDefaultAspectRatio, + mDefaultMinSize, displayLayout.width(), displayLayout.height()); + } + + // Now that we have the default size, apply the snap fraction if valid or position the + // bounds using the default gravity. + if (snapFraction != INVALID_SNAP_FRACTION) { + defaultBounds.set(0, 0, defaultSize.getWidth(), defaultSize.getHeight()); + final Rect movementBounds = getMovementBounds(defaultBounds); + mSnapAlgorithm.applySnapFraction(defaultBounds, movementBounds, snapFraction); + } else { + Gravity.apply(mDefaultStackGravity, defaultSize.getWidth(), defaultSize.getHeight(), + insetBounds, 0, Math.max( + mPipBoundsState.isImeShowing() ? mPipBoundsState.getImeHeight() : 0, + mPipBoundsState.isShelfShowing() + ? mPipBoundsState.getShelfHeight() : 0), defaultBounds); + } + return defaultBounds; + } + + /** + * Populates the bounds on the screen that the PIP can be visible in. + */ + public void getInsetBounds(Rect outRect) { + final DisplayLayout displayLayout = mPipBoundsState.getDisplayLayout(); + Rect insets = mPipBoundsState.getDisplayLayout().stableInsets(); + outRect.set(insets.left + mScreenEdgeInsets.x, + insets.top + mScreenEdgeInsets.y, + displayLayout.width() - insets.right - mScreenEdgeInsets.x, + displayLayout.height() - insets.bottom - mScreenEdgeInsets.y); + } + + /** + * @return the movement bounds for the given stackBounds and the current state of the + * controller. + */ + public Rect getMovementBounds(Rect stackBounds) { + return getMovementBounds(stackBounds, true /* adjustForIme */); + } + + /** + * @return the movement bounds for the given stackBounds and the current state of the + * controller. + */ + public Rect getMovementBounds(Rect stackBounds, boolean adjustForIme) { + final Rect movementBounds = new Rect(); + getInsetBounds(movementBounds); + + // Apply the movement bounds adjustments based on the current state. + getMovementBounds(stackBounds, movementBounds, movementBounds, + (adjustForIme && mPipBoundsState.isImeShowing()) + ? mPipBoundsState.getImeHeight() : 0); + + return movementBounds; + } + + /** + * Adjusts movementBoundsOut so that it is the movement bounds for the given 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 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); + } + + public int getDefaultMinSize() { + return mDefaultMinSize; + } + + /** + * @return the pixels for a given dp value. + */ + private int dpToPx(float dpValue, DisplayMetrics dm) { + return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm); + } + + /** + * @return the size of the PiP at the given aspectRatio, ensuring that the minimum edge + * is at least 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); + } + + /** + * Dumps internal states. + */ + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mDefaultAspectRatio=" + mDefaultAspectRatio); + pw.println(innerPrefix + "mMinAspectRatio=" + mMinAspectRatio); + pw.println(innerPrefix + "mMaxAspectRatio=" + mMaxAspectRatio); + pw.println(innerPrefix + "mDefaultStackGravity=" + mDefaultStackGravity); + 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..b112c51455d2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java @@ -0,0 +1,489 @@ +/* + * 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.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.Size; +import android.view.Display; +import android.view.DisplayInfo; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.function.TriConsumer; +import com.android.wm.shell.R; +import com.android.wm.shell.common.DisplayLayout; + +import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** + * Singleton source of truth for the current state of PIP bounds. + */ +public final class PipBoundsState { + public static final int STASH_TYPE_NONE = 0; + public static final int STASH_TYPE_LEFT = 1; + public static final int STASH_TYPE_RIGHT = 2; + + @IntDef(prefix = { "STASH_TYPE_" }, value = { + STASH_TYPE_NONE, + STASH_TYPE_LEFT, + STASH_TYPE_RIGHT + }) + @Retention(RetentionPolicy.SOURCE) + @interface StashType {} + + private static final String TAG = PipBoundsState.class.getSimpleName(); + + private final @NonNull Rect mBounds = new Rect(); + private final @NonNull Rect mMovementBounds = new Rect(); + private final @NonNull Rect mNormalBounds = new Rect(); + private final @NonNull Rect mExpandedBounds = new Rect(); + private final @NonNull Rect mNormalMovementBounds = new Rect(); + private final @NonNull Rect mExpandedMovementBounds = new Rect(); + private final Point mMaxSize = new Point(); + private final Point mMinSize = new Point(); + private final @NonNull Context mContext; + private float mAspectRatio; + private int mStashedState = STASH_TYPE_NONE; + private int mStashOffset; + private @Nullable PipReentryState mPipReentryState; + private @Nullable ComponentName mLastPipComponentName; + private int mDisplayId = Display.DEFAULT_DISPLAY; + private final @NonNull DisplayLayout mDisplayLayout = new DisplayLayout(); + /** The current minimum edge size of PIP. */ + private int mMinEdgeSize; + /** The preferred minimum (and default) size specified by apps. */ + private @Nullable Size mOverrideMinSize; + private final @NonNull MotionBoundsState mMotionBoundsState = new MotionBoundsState(); + private boolean mIsImeShowing; + private int mImeHeight; + private boolean mIsShelfShowing; + private int mShelfHeight; + /** Whether the user has resized the PIP manually. */ + private boolean mHasUserResizedPip; + + private @Nullable Runnable mOnMinimalSizeChangeCallback; + private @Nullable TriConsumer<Boolean, Integer, Boolean> mOnShelfVisibilityChangeCallback; + + public PipBoundsState(@NonNull Context context) { + mContext = context; + reloadResources(); + } + + /** Reloads the resources. */ + public void onConfigurationChanged() { + reloadResources(); + } + + private void reloadResources() { + mStashOffset = mContext.getResources().getDimensionPixelSize(R.dimen.pip_stash_offset); + } + + /** Set the current PIP bounds. */ + public void setBounds(@NonNull Rect bounds) { + mBounds.set(bounds); + } + + /** Get the current PIP bounds. */ + @NonNull + public Rect getBounds() { + return new Rect(mBounds); + } + + /** Returns the current movement bounds. */ + @NonNull + public Rect getMovementBounds() { + return mMovementBounds; + } + + /** Set the current normal PIP bounds. */ + public void setNormalBounds(@NonNull Rect bounds) { + mNormalBounds.set(bounds); + } + + /** Get the current normal PIP bounds. */ + @NonNull + public Rect getNormalBounds() { + return mNormalBounds; + } + + /** Set the expanded bounds of PIP. */ + public void setExpandedBounds(@NonNull Rect bounds) { + mExpandedBounds.set(bounds); + } + + /** Get the PIP expanded bounds. */ + @NonNull + public Rect getExpandedBounds() { + return mExpandedBounds; + } + + /** Set the normal movement bounds. */ + public void setNormalMovementBounds(@NonNull Rect bounds) { + mNormalMovementBounds.set(bounds); + } + + /** Returns the normal movement bounds. */ + @NonNull + public Rect getNormalMovementBounds() { + return mNormalMovementBounds; + } + + /** Set the expanded movement bounds. */ + public void setExpandedMovementBounds(@NonNull Rect bounds) { + mExpandedMovementBounds.set(bounds); + } + + /** Sets the max possible size for resize. */ + public void setMaxSize(int width, int height) { + mMaxSize.set(width, height); + } + + /** Sets the min possible size for resize. */ + public void setMinSize(int width, int height) { + mMinSize.set(width, height); + } + + public Point getMaxSize() { + return mMaxSize; + } + + public Point getMinSize() { + return mMinSize; + } + + /** Returns the expanded movement bounds. */ + @NonNull + public Rect getExpandedMovementBounds() { + return mExpandedMovementBounds; + } + + /** Dictate where PiP currently should be stashed, if at all. */ + public void setStashed(@StashType int stashedState) { + mStashedState = stashedState; + } + + /** + * Return where the PiP is stashed, if at all. + * @return {@code STASH_NONE}, {@code STASH_LEFT} or {@code STASH_RIGHT}. + */ + public @StashType int getStashedState() { + return mStashedState; + } + + /** Whether PiP is stashed or not. */ + public boolean isStashed() { + return mStashedState != STASH_TYPE_NONE; + } + + /** Returns the offset from the edge of the screen for PiP stash. */ + public int getStashOffset() { + return mStashOffset; + } + + /** Set the PIP aspect ratio. */ + public void setAspectRatio(float aspectRatio) { + mAspectRatio = aspectRatio; + } + + /** Get the PIP aspect ratio. */ + public float getAspectRatio() { + return mAspectRatio; + } + + /** Save the reentry state to restore to when re-entering PIP mode. */ + public void saveReentryState(Size size, float fraction) { + mPipReentryState = new PipReentryState(size, fraction); + } + + /** Returns the saved reentry state. */ + @Nullable + public PipReentryState getReentryState() { + return mPipReentryState; + } + + /** Set the last {@link ComponentName} to enter PIP mode. */ + public void setLastPipComponentName(@Nullable ComponentName lastPipComponentName) { + final boolean changed = !Objects.equals(mLastPipComponentName, lastPipComponentName); + mLastPipComponentName = lastPipComponentName; + if (changed) { + clearReentryState(); + setHasUserResizedPip(false); + } + } + + /** Get the last PIP component name, if any. */ + @Nullable + public ComponentName getLastPipComponentName() { + return mLastPipComponentName; + } + + /** Get the current display id. */ + public int getDisplayId() { + return mDisplayId; + } + + /** Set the current display id for the associated display layout. */ + public void setDisplayId(int displayId) { + mDisplayId = displayId; + } + + /** Returns the display's bounds. */ + @NonNull + public Rect getDisplayBounds() { + return new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); + } + + /** Update the display layout. */ + public void setDisplayLayout(@NonNull DisplayLayout displayLayout) { + mDisplayLayout.set(displayLayout); + } + + /** Get the display layout. */ + @NonNull + public DisplayLayout getDisplayLayout() { + return mDisplayLayout; + } + + @VisibleForTesting + void clearReentryState() { + mPipReentryState = null; + } + + /** Set the PIP minimum edge size. */ + public void setMinEdgeSize(int minEdgeSize) { + mMinEdgeSize = minEdgeSize; + } + + /** Returns the PIP's current minimum edge size. */ + public int getMinEdgeSize() { + return mMinEdgeSize; + } + + /** Sets the preferred size of PIP as specified by the activity in PIP mode. */ + public void setOverrideMinSize(@Nullable Size overrideMinSize) { + final boolean changed = !Objects.equals(overrideMinSize, mOverrideMinSize); + mOverrideMinSize = overrideMinSize; + if (changed && mOnMinimalSizeChangeCallback != null) { + mOnMinimalSizeChangeCallback.run(); + } + } + + /** Returns the preferred minimal size specified by the activity in PIP. */ + @Nullable + public Size getOverrideMinSize() { + return mOverrideMinSize; + } + + /** Returns the minimum edge size of the override minimum size, or 0 if not set. */ + public int getOverrideMinEdgeSize() { + if (mOverrideMinSize == null) return 0; + return Math.min(mOverrideMinSize.getWidth(), mOverrideMinSize.getHeight()); + } + + /** Get the state of the bounds in motion. */ + @NonNull + public MotionBoundsState getMotionBoundsState() { + return mMotionBoundsState; + } + + /** Set whether the IME is currently showing and its height. */ + public void setImeVisibility(boolean imeShowing, int imeHeight) { + mIsImeShowing = imeShowing; + mImeHeight = imeHeight; + } + + /** Returns whether the IME is currently showing. */ + public boolean isImeShowing() { + return mIsImeShowing; + } + + /** Returns the IME height. */ + public int getImeHeight() { + return mImeHeight; + } + + /** Set whether the shelf is showing and its height. */ + public void setShelfVisibility(boolean showing, int height) { + setShelfVisibility(showing, height, true); + } + + /** Set whether the shelf is showing and its height. */ + public void setShelfVisibility(boolean showing, int height, boolean updateMovementBounds) { + final boolean shelfShowing = showing && height > 0; + if (shelfShowing == mIsShelfShowing && height == mShelfHeight) { + return; + } + + mIsShelfShowing = showing; + mShelfHeight = height; + if (mOnShelfVisibilityChangeCallback != null) { + mOnShelfVisibilityChangeCallback.accept(mIsShelfShowing, mShelfHeight, + updateMovementBounds); + } + } + + /** Returns whether the shelf is currently showing. */ + public boolean isShelfShowing() { + return mIsShelfShowing; + } + + /** Returns the shelf height. */ + public int getShelfHeight() { + return mShelfHeight; + } + + /** Returns whether the user has resized the PIP. */ + public boolean hasUserResizedPip() { + return mHasUserResizedPip; + } + + /** Set whether the user has resized the PIP. */ + public void setHasUserResizedPip(boolean hasUserResizedPip) { + mHasUserResizedPip = hasUserResizedPip; + } + + /** + * Registers a callback when the minimal size of PIP that is set by the app changes. + */ + public void setOnMinimalSizeChangeCallback(@Nullable Runnable onMinimalSizeChangeCallback) { + mOnMinimalSizeChangeCallback = onMinimalSizeChangeCallback; + } + + /** Set a callback to be notified when the shelf visibility changes. */ + public void setOnShelfVisibilityChangeCallback( + @Nullable TriConsumer<Boolean, Integer, Boolean> onShelfVisibilityChangeCallback) { + mOnShelfVisibilityChangeCallback = onShelfVisibilityChangeCallback; + } + + /** Source of truth for the current bounds of PIP that may be in motion. */ + public static class MotionBoundsState { + /** The bounds used when PIP is in motion (e.g. during a drag or animation) */ + private final @NonNull Rect mBoundsInMotion = new Rect(); + /** The destination bounds to which PIP is animating. */ + private final @NonNull Rect mAnimatingToBounds = new Rect(); + + /** Whether PIP is being dragged or animated (e.g. resizing, in fling, etc). */ + public boolean isInMotion() { + return !mBoundsInMotion.isEmpty(); + } + + /** Set the temporary bounds used to represent the drag or animation bounds of PIP. */ + public void setBoundsInMotion(@NonNull Rect bounds) { + mBoundsInMotion.set(bounds); + } + + /** Set the bounds to which PIP is animating. */ + public void setAnimatingToBounds(@NonNull Rect bounds) { + mAnimatingToBounds.set(bounds); + } + + /** Called when all ongoing motion operations have ended. */ + public void onAllAnimationsEnded() { + mBoundsInMotion.setEmpty(); + } + + /** Called when an ongoing physics animation has ended. */ + public void onPhysicsAnimationEnded() { + mAnimatingToBounds.setEmpty(); + } + + /** Returns the motion bounds. */ + @NonNull + public Rect getBoundsInMotion() { + return mBoundsInMotion; + } + + /** Returns the destination bounds to which PIP is currently animating. */ + @NonNull + public Rect getAnimatingToBounds() { + return mAnimatingToBounds; + } + + void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + MotionBoundsState.class.getSimpleName()); + pw.println(innerPrefix + "mBoundsInMotion=" + mBoundsInMotion); + pw.println(innerPrefix + "mAnimatingToBounds=" + mAnimatingToBounds); + } + } + + static final class PipReentryState { + private static final String TAG = PipReentryState.class.getSimpleName(); + + private final @Nullable Size mSize; + private final float mSnapFraction; + + PipReentryState(@Nullable Size size, float snapFraction) { + mSize = size; + mSnapFraction = snapFraction; + } + + @Nullable + 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 + "mNormalBounds=" + mNormalBounds); + pw.println(innerPrefix + "mExpandedBounds=" + mExpandedBounds); + pw.println(innerPrefix + "mMovementBounds=" + mMovementBounds); + pw.println(innerPrefix + "mNormalMovementBounds=" + mNormalMovementBounds); + pw.println(innerPrefix + "mExpandedMovementBounds=" + mExpandedMovementBounds); + pw.println(innerPrefix + "mLastPipComponentName=" + mLastPipComponentName); + pw.println(innerPrefix + "mAspectRatio=" + mAspectRatio); + pw.println(innerPrefix + "mDisplayId=" + mDisplayId); + pw.println(innerPrefix + "mDisplayLayout=" + mDisplayLayout); + pw.println(innerPrefix + "mStashedState=" + mStashedState); + pw.println(innerPrefix + "mStashOffset=" + mStashOffset); + pw.println(innerPrefix + "mMinEdgeSize=" + mMinEdgeSize); + pw.println(innerPrefix + "mOverrideMinSize=" + mOverrideMinSize); + pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing); + pw.println(innerPrefix + "mImeHeight=" + mImeHeight); + pw.println(innerPrefix + "mIsShelfShowing=" + mIsShelfShowing); + pw.println(innerPrefix + "mShelfHeight=" + mShelfHeight); + if (mPipReentryState == null) { + pw.println(innerPrefix + "mPipReentryState=null"); + } else { + mPipReentryState.dump(pw, innerPrefix); + } + mMotionBoundsState.dump(pw, innerPrefix); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java new file mode 100644 index 000000000000..1a4616c5f591 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java @@ -0,0 +1,310 @@ +/* + * 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.PendingIntent.FLAG_IMMUTABLE; +import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; + +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.MediaMetadata; +import android.media.session.MediaController; +import android.media.session.MediaSessionManager; +import android.media.session.PlaybackState; +import android.os.Handler; +import android.os.UserHandle; + +import androidx.annotation.Nullable; + +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.PLAY"; + private static final String ACTION_PAUSE = "com.android.wm.shell.pip.PAUSE"; + private static final String ACTION_NEXT = "com.android.wm.shell.pip.NEXT"; + private static final String ACTION_PREV = "com.android.wm.shell.pip.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); + } + + /** + * A listener interface to receive notification on changes to the media metadata. + */ + public interface MetadataListener { + /** + * Called when the media metadata changes. + */ + void onMediaMetadataChanged(MediaMetadata metadata); + } + + private final Context mContext; + private final Handler mMainHandler; + + 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(); + } + + @Override + public void onMetadataChanged(@Nullable MediaMetadata metadata) { + notifyMetadataChanged(metadata); + } + }; + + private final MediaSessionManager.OnActiveSessionsChangedListener mSessionsChangedListener = + this::resolveActiveMediaController; + + private final ArrayList<ActionListener> mActionListeners = new ArrayList<>(); + private final ArrayList<MetadataListener> mMetadataListeners = new ArrayList<>(); + + public PipMediaController(Context context, Handler mainHandler) { + mContext = context; + mMainHandler = mainHandler; + IntentFilter mediaControlFilter = new IntentFilter(); + mediaControlFilter.addAction(ACTION_PLAY); + mediaControlFilter.addAction(ACTION_PAUSE); + mediaControlFilter.addAction(ACTION_NEXT); + mediaControlFilter.addAction(ACTION_PREV); + mContext.registerReceiverForAllUsers(mPlayPauseActionReceiver, mediaControlFilter, + null /* permission */, mainHandler); + + createMediaActions(); + mMediaSessionManager = context.getSystemService(MediaSessionManager.class); + } + + /** + * 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.CURRENT)); + } + + /** + * Adds a new media action listener. + */ + public void addActionListener(ActionListener listener) { + if (!mActionListeners.contains(listener)) { + mActionListeners.add(listener); + listener.onMediaActionsChanged(getMediaActions()); + } + } + + /** + * Removes a media action listener. + */ + public void removeActionListener(ActionListener listener) { + listener.onMediaActionsChanged(Collections.emptyList()); + mActionListeners.remove(listener); + } + + /** + * Adds a new media metadata listener. + */ + public void addMetadataListener(MetadataListener listener) { + if (!mMetadataListeners.contains(listener)) { + mMetadataListeners.add(listener); + listener.onMediaMetadataChanged(getMediaMetadata()); + } + } + + /** + * Removes a media metadata listener. + */ + public void removeMetadataListener(MetadataListener listener) { + listener.onMediaMetadataChanged(null); + mMetadataListeners.remove(listener); + } + + private MediaMetadata getMediaMetadata() { + return mMediaController != null ? mMediaController.getMetadata() : null; + } + + /** + * Gets the set of media actions currently available. + */ + private List<RemoteAction> getMediaActions() { + if (mMediaController == null || mMediaController.getPlaybackState() == null) { + return Collections.emptyList(); + } + + 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.CURRENT, mMainHandler); + } + + /** + * 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).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, mMainHandler); + } + notifyActionsChanged(); + notifyMetadataChanged(getMediaMetadata()); + + // 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 (!mActionListeners.isEmpty()) { + List<RemoteAction> actions = getMediaActions(); + mActionListeners.forEach(l -> l.onMediaActionsChanged(actions)); + } + } + + /** + * Notifies all listeners that the metadata have changed. + */ + private void notifyMetadataChanged(MediaMetadata metadata) { + if (!mMetadataListeners.isEmpty()) { + mMetadataListeners.forEach(l -> l.onMediaMetadataChanged(metadata)); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java new file mode 100644 index 000000000000..8d9ad4d1b96c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java @@ -0,0 +1,106 @@ +/* + * 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.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; +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.PRIVATE_FLAG_TRUSTED_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + +import android.annotation.Nullable; +import android.app.ActivityManager.RunningTaskInfo; +import android.app.RemoteAction; +import android.content.pm.ParceledListSlice; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.view.SurfaceControl; +import android.view.WindowManager; + +/** + * Interface to allow {@link com.android.wm.shell.pip.PipTaskOrganizer} to call into + * PiP menu when certain events happen (task appear/vanish, PiP move, etc.) + */ +public interface PipMenuController { + + String MENU_WINDOW_TITLE = "PipMenuView"; + + /** + * Called when + * {@link PipTaskOrganizer#onTaskAppeared(RunningTaskInfo, SurfaceControl)} + * is called. + */ + void attach(SurfaceControl leash); + + /** + * Called when + * {@link PipTaskOrganizer#onTaskVanished(RunningTaskInfo)} is called. + */ + void detach(); + + /** + * Check if menu is visible or not. + */ + boolean isMenuVisible(); + + /** + * Show the PIP menu. + */ + void showMenu(); + + /** + * Given a set of actions, update the menu. + */ + void setAppActions(ParceledListSlice<RemoteAction> appActions); + + /** + * Resize the PiP menu with the given bounds. The PiP SurfaceControl is given if there is a + * need to synchronize the movements on the same frame as PiP. + */ + default void resizePipMenu(@Nullable SurfaceControl pipLeash, + @Nullable SurfaceControl.Transaction t, + Rect destinationBounds) {} + + /** + * Move the PiP menu with the given bounds. The PiP SurfaceControl is given if there is a + * need to synchronize the movements on the same frame as PiP. + */ + default void movePipMenu(@Nullable SurfaceControl pipLeash, + @Nullable SurfaceControl.Transaction t, + Rect destinationBounds) {} + + /** + * Update the PiP menu with the given bounds for re-layout purposes. + */ + default void updateMenuBounds(Rect destinationBounds) {} + + /** + * Returns a default LayoutParams for the PIP Menu. + * @param width the PIP stack width. + * @param height the PIP stack height. + */ + default WindowManager.LayoutParams getPipMenuLayoutParams(String title, int width, int height) { + final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(width, height, + TYPE_APPLICATION_OVERLAY, + FLAG_WATCH_OUTSIDE_TOUCH | FLAG_SPLIT_TOUCH | FLAG_SLIPPERY | FLAG_NOT_TOUCHABLE, + PixelFormat.TRANSLUCENT); + lp.privateFlags |= PRIVATE_FLAG_TRUSTED_OVERLAY; + lp.setTitle(title); + return lp; + } +} 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..0528e4d88374 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSnapAlgorithm.java @@ -0,0 +1,158 @@ +/* + * 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.PipBoundsState.STASH_TYPE_LEFT; +import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE; +import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT; + +import android.graphics.Rect; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * 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 { + + /** + * Returns a fraction that describes where the PiP bounds is. + * See {@link #getSnapFraction(Rect, Rect, int)}. + */ + public float getSnapFraction(Rect stackBounds, Rect movementBounds) { + return getSnapFraction(stackBounds, movementBounds, STASH_TYPE_NONE); + } + + /** + * @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. + * stashType dictates whether the PiP is stashed (off-screen) or not. If + * that's the case, we will have to do some math to calculate the snap fraction + * correctly. + * + * 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, + @PipBoundsState.StashType int stashType) { + final Rect tmpBounds = new Rect(); + snapRectToClosestEdge(stackBounds, movementBounds, tmpBounds, stashType); + 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); + } + } + + /** + * Same as {@link #applySnapFraction(Rect, Rect, float)}, but take stash state into + * consideration. + */ + public void applySnapFraction(Rect stackBounds, Rect movementBounds, float snapFraction, + @PipBoundsState.StashType int stashType, int stashOffset, Rect displayBounds) { + applySnapFraction(stackBounds, movementBounds, snapFraction); + + if (stashType != STASH_TYPE_NONE) { + stackBounds.offsetTo(stashType == STASH_TYPE_LEFT + ? stashOffset - stackBounds.width() + : displayBounds.right - stashOffset, + stackBounds.top); + } + } + + /** + * Snaps the {@param stackBounds} to the closest edge of the {@param movementBounds} and writes + * the new bounds out to {@param boundsOut}. + */ + @VisibleForTesting + void snapRectToClosestEdge(Rect stackBounds, Rect movementBounds, Rect boundsOut, + @PipBoundsState.StashType int stashType) { + int leftEdge = stackBounds.left; + if (stashType == STASH_TYPE_LEFT) { + leftEdge = movementBounds.left; + } else if (stashType == STASH_TYPE_RIGHT) { + leftEdge = movementBounds.right; + } + final int boundedLeft = Math.max(movementBounds.left, Math.min(movementBounds.right, + leftEdge)); + 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(leftEdge - movementBounds.left); + final int fromTop = Math.abs(stackBounds.top - movementBounds.top); + final int fromRight = Math.abs(movementBounds.right - leftEdge); + 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..a777a2766ee7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java @@ -0,0 +1,169 @@ +/* + * 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) { + return scale(tx, leash, sourceBounds, destinationBounds, 0 /* degrees */); + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash, along with a rotation. + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect sourceBounds, Rect destinationBounds, float degrees) { + mTmpSourceRectF.set(sourceBounds); + mTmpDestinationRectF.set(destinationBounds); + mTmpTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL); + mTmpTransform.postRotate(degrees); + 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; + } + + /** + * Re-parents the snapshot to the parent's surface control and shows it. + */ + public PipSurfaceTransactionHelper reparentAndShowSurfaceSnapshot( + SurfaceControl.Transaction t, SurfaceControl parent, SurfaceControl snapshot) { + t.reparent(snapshot, parent); + t.setLayer(snapshot, Integer.MAX_VALUE); + t.show(snapshot); + t.apply(); + 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..b7958b7a7077 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -0,0 +1,1180 @@ +/* + * 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_EXPAND_OR_UNEXPAND; +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_SNAP_AFTER_RESIZE; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE; +import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection; +import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +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.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.util.Rational; +import android.util.Size; +import android.view.Display; +import android.view.SurfaceControl; +import android.window.TaskOrganizer; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; +import android.window.WindowContainerTransactionCallback; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.jank.InteractionJankMonitor; +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; +import com.android.wm.shell.pip.phone.PipMotionHelper; +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; + + // 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 PipBoundsState mPipBoundsState; + private final PipBoundsAlgorithm mPipBoundsAlgorithm; + private final @NonNull PipMenuController mPipMenuController; + 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<LegacySplitScreen> mSplitScreenOptional; + protected final ShellTaskOrganizer mTaskOrganizer; + protected final ShellExecutor mMainExecutor; + + // 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) { + // TODO (b//169221267): Add jank listener for transactions without buffer updates. + //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) { + // TODO (b//169221267): Add jank listener for transactions without buffer updates. + //InteractionJankMonitor.getInstance().end( + // InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP); + } + } + + @Override + public void onPipAnimationCancel(PipAnimationController.PipTransitionAnimator animator) { + sendOnPipTransitionCancelled(animator.getTransitionDirection()); + } + }; + + 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 mWaitForFixedRotation; + + /** + * 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 mInSwipePipToHomeTransition; + + public PipTaskOrganizer(Context context, @NonNull PipBoundsState pipBoundsState, + @NonNull PipBoundsAlgorithm boundsHandler, + @NonNull PipMenuController pipMenuController, + @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, + Optional<LegacySplitScreen> splitScreenOptional, + @NonNull DisplayController displayController, + @NonNull PipUiEventLogger pipUiEventLogger, + @NonNull ShellTaskOrganizer shellTaskOrganizer, + @ShellMainThread ShellExecutor mainExecutor) { + mPipBoundsState = pipBoundsState; + mPipBoundsAlgorithm = boundsHandler; + mPipMenuController = pipMenuController; + mEnterExitAnimationDuration = context.getResources() + .getInteger(R.integer.config_pipResizeAnimationDuration); + mSurfaceTransactionHelper = surfaceTransactionHelper; + mPipAnimationController = new PipAnimationController(mSurfaceTransactionHelper); + mPipUiEventLoggerLogger = pipUiEventLogger; + mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; + mSplitScreenOptional = splitScreenOptional; + mTaskOrganizer = shellTaskOrganizer; + mMainExecutor = mainExecutor; + + // TODO: Can be removed once wm components are created on the shell-main thread + mMainExecutor.execute(() -> { + mTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_PIP); + }); + displayController.addDisplayWindowListener(this); + } + + 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() && mWaitForFixedRotation; + } + + /** + * 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) { + mInSwipePipToHomeTransition = true; + sendOnPipTransitionStarted(componentName, TRANSITION_DIRECTION_TO_PIP); + setBoundsStateForEntry(componentName, pictureInPictureParams, activityInfo); + // disable the conflicting transaction from fixed rotation, see also + // onFixedRotationStarted and onFixedRotationFinished + mWaitForFixedRotation = false; + return mPipBoundsAlgorithm.getEntryDestinationBounds(); + } + + /** + * 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 (mInSwipePipToHomeTransition) { + mPipBoundsState.setBounds(destinationBounds); + } + } + + private void setBoundsStateForEntry(ComponentName componentName, PictureInPictureParams params, + ActivityInfo activityInfo) { + mPipBoundsState.setLastPipComponentName(componentName); + mPipBoundsState.setAspectRatio(getAspectRatioOrDefault(params)); + mPipBoundsState.setOverrideMinSize(getMinimalSize(activityInfo)); + } + + /** + * 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() + != mPipBoundsState.getDisplayLayout().rotation(); + 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) { + mMainExecutor.execute(() -> { + 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, + 0 /* startingAngle */, 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. + 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; + setBoundsStateForEntry(mTaskInfo.topActivity, mPictureInPictureParams, + mTaskInfo.topActivityInfo); + + 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 != mPipBoundsState.getDisplayId() + && mOnDisplayIdChangeCallback != null) { + mOnDisplayIdChangeCallback.accept(info.displayId); + } + + if (mInSwipePipToHomeTransition) { + final Rect destinationBounds = mPipBoundsState.getBounds(); + // animation is finished in the Launcher and here we directly apply the final touch. + applyEnterPipSyncTransaction(destinationBounds, () -> { + // ensure menu's settled in its final bounds first + finishResizeForMenu(destinationBounds); + sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); + }); + mInSwipePipToHomeTransition = false; + return; + } + + if (mWaitForFixedRotation) { + 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; + } + + final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); + Objects.requireNonNull(destinationBounds, "Missing destination bounds"); + final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); + + if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { + mPipMenuController.attach(mLeash); + final Rect sourceHintRect = getValidSourceHintRect(info.pictureInPictureParams, + currentBounds); + scheduleAnimateResizePip(currentBounds, destinationBounds, 0 /* startingAngle */, + 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; + } + + @VisibleForTesting + 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, () -> { + 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) { + // PiP menu is attached late in the process here to avoid any artifacts on the leash + // caused by addShellRoot when in gesture navigation mode. + mPipMenuController.attach(mLeash); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); + wct.setBounds(mToken, destinationBounds); + wct.scheduleFinishEnterPip(mToken, destinationBounds); + // TODO: Migrate to SyncTransactionQueue + mTaskOrganizer.applySyncTransaction(wct, new WindowContainerTransactionCallback() { + @Override + public void onTransactionReady(int id, SurfaceControl.Transaction t) { + mMainExecutor.execute(() -> { + 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(); + 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; + } + 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) { + for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { + final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); + callback.onPipTransitionCanceled(mTaskInfo.baseActivity, direction); + } + } + + /** + * 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; + } + mWaitForFixedRotation = false; + mInSwipePipToHomeTransition = false; + mPictureInPictureParams = null; + mState = State.UNDEFINED; + mPipUiEventLoggerLogger.setTaskInfo(null); + mPipMenuController.detach(); + + if (info.displayId != Display.DEFAULT_DISPLAY && mOnDisplayIdChangeCallback != null) { + mOnDisplayIdChangeCallback.accept(Display.DEFAULT_DISPLAY); + } + } + + @Override + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) { + Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken"); + mPipBoundsState.setLastPipComponentName(info.topActivity); + mPipBoundsState.setOverrideMinSize(getMinimalSize(info.topActivityInfo)); + 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 bounds if valid. + final Rect destinationBounds = mPipBoundsAlgorithm.getAdjustedDestinationBounds( + mPipBoundsState.getBounds(), mPipBoundsState.getAspectRatio()); + Objects.requireNonNull(destinationBounds, "Missing destination bounds"); + scheduleAnimateResizePip(destinationBounds, mEnterExitAnimationDuration, + null /* updateBoundsCallback */); + } + + @Override + public void onFixedRotationStarted(int displayId, int newRotation) { + mWaitForFixedRotation = true; + } + + @Override + public void onFixedRotationFinished(int displayId) { + if (mWaitForFixedRotation && mState.isInPip()) { + final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); + // schedule a regular animation to ensure all the callbacks are still being sent + enterPipWithAlphaAnimation(destinationBounds, 0 /* durationMs */); + } + mWaitForFixedRotation = 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) { + // note that this can be called when swiping pip to home is happening. For instance, + // swiping an app in landscape to portrait home. skip this entirely if that's the case. + if (mInSwipePipToHomeTransition && fromRotation) { + if (DEBUG) Log.d(TAG, "skip onMovementBoundsChanged due to swipe-pip-to-home"); + return; + } + final PipAnimationController.PipTransitionAnimator animator = + mPipAnimationController.getCurrentAnimator(); + if (animator == null || !animator.isRunning() + || animator.getTransitionDirection() != TRANSITION_DIRECTION_TO_PIP) { + if (mState.isInPip() && fromRotation) { + // Update bounds state to final destination first. It's important to do this + // before finishing & cancelling the transition animation so that the MotionHelper + // bounds are synchronized to the destination bounds when the animation ends. + mPipBoundsState.setBounds(destinationBoundsOut); + // 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); + } + + // 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 + && mPipBoundsState.getDisplayBounds().contains(currentDestinationBounds)) { + // no need to update the destination bounds, bail early + return; + } + + final Rect newDestinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); + 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) { + scheduleAnimateResizePip(toBounds, duration, TRANSITION_DIRECTION_NONE, + updateBoundsCallback); + } + + /** + * Animates resizing of the pinned stack given the duration. + */ + public void scheduleAnimateResizePip(Rect toBounds, int duration, + @PipAnimationController.TransitionDirection int direction, + Consumer<Rect> updateBoundsCallback) { + if (mWaitForFixedRotation) { + Log.d(TAG, "skip scheduleAnimateResizePip, entering pip deferred"); + return; + } + scheduleAnimateResizePip(mPipBoundsState.getBounds(), toBounds, 0 /* startingAngle */, + null /* sourceHintRect */, direction, duration, updateBoundsCallback); + } + + /** + * Animates resizing of the pinned stack given the duration and start bounds. + * This is used when the starting bounds is not the current PiP bounds. + */ + public void scheduleAnimateResizePip(Rect fromBounds, Rect toBounds, int duration, + float startingAngle, Consumer<Rect> updateBoundsCallback) { + if (mWaitForFixedRotation) { + Log.d(TAG, "skip scheduleAnimateResizePip, entering pip deferred"); + return; + } + scheduleAnimateResizePip(fromBounds, toBounds, startingAngle, null /* sourceHintRect */, + TRANSITION_DIRECTION_SNAP_AFTER_RESIZE, duration, updateBoundsCallback); + } + + /** + * Animates resizing of the pinned stack given the duration and start bounds. + * This always animates the angle to zero from the starting angle. + */ + private void scheduleAnimateResizePip(Rect currentBounds, Rect destinationBounds, + float startingAngle, 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; + } + + animateResizePip(currentBounds, destinationBounds, sourceHintRect, direction, durationMs, + startingAngle); + if (updateBoundsCallback != null) { + updateBoundsCallback.accept(destinationBounds); + } + } + + /** + * 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) { + // Could happen when exitPip + if (mToken == null || mLeash == null) { + Log.w(TAG, "Abort animation, invalid leash"); + return; + } + mPipBoundsState.setBounds(toBounds); + final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); + mSurfaceTransactionHelper + .crop(tx, mLeash, toBounds) + .round(tx, mLeash, mState.isInPip()); + if (mPipMenuController.isMenuVisible()) { + mPipMenuController.resizePipMenu(mLeash, tx, toBounds); + } else { + tx.apply(); + } + if (updateBoundsCallback != null) { + updateBoundsCallback.accept(toBounds); + } + } + + /** + * Directly perform manipulation/resize on the leash, along with rotation. This will not perform + * any {@link WindowContainerTransaction} until {@link #scheduleFinishResizePip} is called. + */ + public void scheduleUserResizePip(Rect startBounds, Rect toBounds, + Consumer<Rect> updateBoundsCallback) { + scheduleUserResizePip(startBounds, toBounds, 0 /* degrees */, updateBoundsCallback); + } + + /** + * 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, float degrees, + Consumer<Rect> updateBoundsCallback) { + // Could happen when exitPip + if (mToken == null || mLeash == null) { + Log.w(TAG, "Abort animation, invalid leash"); + return; + } + + if (startBounds.isEmpty() || toBounds.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, toBounds, degrees); + if (mPipMenuController.isMenuVisible()) { + mPipMenuController.movePipMenu(mLeash, tx, toBounds); + } else { + tx.apply(); + } + if (updateBoundsCallback != null) { + updateBoundsCallback.accept(toBounds); + } + } + + /** + * 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); + } + + /** + * Finish an intermediate resize operation. This is expected to be called after + * {@link #scheduleResizePip}. + * + * @param destinationBounds the final bounds of the PIP after resizing + * @param direction the transition direction + * @param updateBoundsCallback a callback to invoke after finishing the resize + */ + public void scheduleFinishResizePip(Rect destinationBounds, + @PipAnimationController.TransitionDirection int direction, + Consumer<Rect> updateBoundsCallback) { + if (mState.shouldBlockResizeRequest()) { + return; + } + + finishResize(createFinishResizeSurfaceTransaction(destinationBounds), destinationBounds, + direction, -1); + if (updateBoundsCallback != null) { + updateBoundsCallback.accept(destinationBounds); + } + } + + 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 (mWaitForFixedRotation) { + Log.d(TAG, "skip scheduleOffsetPip, entering pip deferred"); + return; + } + offsetPip(originalBounds, 0 /* xOffset */, offset, duration); + Rect toBounds = new Rect(originalBounds); + toBounds.offset(0, offset); + if (updateBoundsCallback != null) { + updateBoundsCallback.accept(toBounds); + } + } + + private void offsetPip(Rect originalBounds, int xOffset, int yOffset, int durationMs) { + 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, 0); + } + + private void finishResize(SurfaceControl.Transaction tx, Rect destinationBounds, + @PipAnimationController.TransitionDirection int direction, + @PipAnimationController.AnimationType int type) { + mPipBoundsState.setBounds(destinationBounds); + if (direction == TRANSITION_DIRECTION_REMOVE_STACK) { + removePipImmediately(); + return; + } else if (isInPipDirection(direction) && type == ANIM_TYPE_ALPHA) { + // TODO: Synchronize this correctly in #applyEnterPipSyncTransaction + finishResizeForMenu(destinationBounds); + return; + } + + WindowContainerTransaction wct = new WindowContainerTransaction(); + prepareFinishResizeTransaction(destinationBounds, direction, tx, wct); + + // Only corner drag, pinch or expand/un-expand resizing may lead to animating the finish + // resize operation. + final boolean mayAnimateFinishResize = direction == TRANSITION_DIRECTION_USER_RESIZE + || direction == TRANSITION_DIRECTION_SNAP_AFTER_RESIZE + || direction == TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND; + // Animate with a cross-fade if enabled and seamless resize is disables by the app. + final boolean animateCrossFadeResize = mayAnimateFinishResize + && !mPictureInPictureParams.isSeamlessResizeEnabled(); + if (animateCrossFadeResize) { + // Take a snapshot of the PIP task and hide it. We'll show it and fade it out after + // the wct transaction is applied and the activity is laid out again. + final SurfaceControl snapshotSurface = mTaskOrganizer.takeScreenshot(mToken); + mSurfaceTransactionHelper.reparentAndShowSurfaceSnapshot( + mSurfaceControlTransactionFactory.getTransaction(), mLeash, snapshotSurface); + mTaskOrganizer.applySyncTransaction(wct, new WindowContainerTransactionCallback() { + @Override + public void onTransactionReady(int id, @NonNull SurfaceControl.Transaction t) { + // Scale the snapshot from its pre-resize bounds to the post-resize bounds. + final Rect snapshotSrc = new Rect(0, 0, snapshotSurface.getWidth(), + snapshotSurface.getHeight()); + final Rect snapshotDest = new Rect(0, 0, destinationBounds.width(), + destinationBounds.height()); + mSurfaceTransactionHelper.scale(t, snapshotSurface, snapshotSrc, snapshotDest); + t.apply(); + + mMainExecutor.execute(() -> { + // Start animation to fade out the snapshot. + final ValueAnimator animator = ValueAnimator.ofFloat(1.0f, 0.0f); + animator.setDuration(mEnterExitAnimationDuration); + animator.addUpdateListener(animation -> { + final float alpha = (float) animation.getAnimatedValue(); + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); + tx.setAlpha(snapshotSurface, alpha); + tx.apply(); + }); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); + tx.remove(snapshotSurface); + tx.apply(); + } + }); + animator.start(); + }); + } + }); + } else { + applyFinishBoundsResize(wct, direction); + } + + finishResizeForMenu(destinationBounds); + } + + private void finishResizeForMenu(Rect destinationBounds) { + mPipMenuController.movePipMenu(null, null, destinationBounds); + mPipMenuController.updateMenuBounds(destinationBounds); + } + + 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, + float startingAngle) { + // Could happen when exitPip + if (mToken == null || mLeash == null) { + Log.w(TAG, "Abort animation, invalid leash"); + return; + } + Rect baseBounds = direction == TRANSITION_DIRECTION_SNAP_AFTER_RESIZE + ? mPipBoundsState.getBounds() : currentBounds; + mPipAnimationController + .getAnimator(mLeash, baseBounds, currentBounds, destinationBounds, sourceHintRect, + direction, startingAngle) + .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() + ? mPipBoundsAlgorithm.getDefaultAspectRatio() + : params.getAspectRatio(); + } + + /** + * Sync with {@link LegacySplitScreen} 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; + } + + LegacySplitScreen legacySplitScreen = mSplitScreenOptional.get(); + if (!legacySplitScreen.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(legacySplitScreen.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..f8b4dd9bc621 --- /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 && taskInfo.topActivity != null) { + mPackageName = taskInfo.topActivity.getPackageName(); + mPackageUid = getUid(mPackageName, taskInfo.userId); + } else { + mPackageName = null; + mPackageUid = INVALID_PACKAGE_UID; + } + } + + /** + * 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/PipUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java new file mode 100644 index 000000000000..da6d9804b29d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java @@ -0,0 +1,58 @@ +/* + * 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 android.app.ActivityTaskManager; +import android.app.ActivityTaskManager.RootTaskInfo; +import android.content.ComponentName; +import android.content.Context; +import android.os.RemoteException; +import android.util.Log; +import android.util.Pair; + +/** A class that includes convenience methods. */ +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) { + 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/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java new file mode 100644 index 000000000000..a57eee83ef59 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java @@ -0,0 +1,536 @@ +/* + * 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 static android.view.WindowManager.SHELL_ROOT_LAYER_PIP; + +import android.annotation.Nullable; +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.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Debug; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.util.Size; +import android.view.MotionEvent; +import android.view.SurfaceControl; +import android.view.SyncRtSurfaceTransactionApplier; +import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams; +import android.view.WindowManagerGlobal; + +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.pip.PipMediaController; +import com.android.wm.shell.pip.PipMediaController.ActionListener; +import com.android.wm.shell.pip.PipMenuController; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * Manages the PiP menu view 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 PhonePipMenuController implements PipMenuController { + + 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 final Matrix mMoveTransform = new Matrix(); + private final Rect mTmpSourceBounds = new Rect(); + private final RectF mTmpSourceRectF = new RectF(); + private final RectF mTmpDestinationRectF = new RectF(); + private final Context mContext; + private final PipMediaController mMediaController; + private final ShellExecutor mMainExecutor; + private final Handler mMainHandler; + + private final ArrayList<Listener> mListeners = new ArrayList<>(); + private final SystemWindows mSystemWindows; + private ParceledListSlice<RemoteAction> mAppActions; + private ParceledListSlice<RemoteAction> mMediaActions; + private SyncRtSurfaceTransactionApplier mApplier; + 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 PhonePipMenuController(Context context, PipMediaController mediaController, + SystemWindows systemWindows, ShellExecutor mainExecutor, + Handler mainHandler) { + mContext = context; + mMediaController = mediaController; + mSystemWindows = systemWindows; + mMainExecutor = mainExecutor; + mMainHandler = mainHandler; + } + + public boolean isMenuVisible() { + return mPipMenuView != null && mMenuState != MENU_STATE_NONE; + } + + /** + * Attach the menu when the PiP task first appears. + */ + @Override + public void attach(SurfaceControl leash) { + attachPipMenuView(); + } + + /** + * Detach the menu when the PiP task is gone. + */ + @Override + public void detach() { + hideMenu(); + detachPipMenuView(); + } + + + void onPinnedStackAnimationEnded() { + if (isMenuVisible()) { + mPipMenuView.onPipAnimationEnded(); + } + } + + private void attachPipMenuView() { + // In case detach was not called (e.g. PIP unexpectedly closed) + if (mPipMenuView != null) { + detachPipMenuView(); + } + mPipMenuView = new PipMenuView(mContext, this, mMainExecutor, mMainHandler); + mSystemWindows.addView(mPipMenuView, + getPipMenuLayoutParams(MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */), + 0, SHELL_ROOT_LAYER_PIP); + } + + private void detachPipMenuView() { + if (mPipMenuView == null) { + return; + } + + mApplier = null; + mSystemWindows.removeView(mPipMenuView); + mPipMenuView = null; + mPipMenuInputToken = null; + } + + /** + * Updates the layout parameters of the menu. + * @param destinationBounds New Menu bounds. + */ + @Override + public void updateMenuBounds(Rect destinationBounds) { + mSystemWindows.updateViewLayout(mPipMenuView, + getPipMenuLayoutParams(MENU_WINDOW_TITLE, destinationBounds.width(), + destinationBounds.height())); + } + + /** + * Tries to grab a surface control from {@link PipMenuView}. If this isn't available for some + * reason (ie. the window isn't ready yet, thus {@link android.view.ViewRootImpl} is + * {@code null}), it will get the leash that the WindowlessWM has assigned to it. + */ + public SurfaceControl getSurfaceControl() { + SurfaceControl sf = mPipMenuView.getWindowSurfaceControl(); + return sf != null ? sf : mSystemWindows.getViewSurface(mPipMenuView); + } + + /** + * Adds a new menu activity listener. + */ + public void addListener(Listener listener) { + if (!mListeners.contains(listener)) { + mListeners.add(listener); + } + } + + @Nullable + Size getEstimatedMinMenuSize() { + return mPipMenuView == null ? null : mPipMenuView.getEstimatedMinMenuSize(); + } + + /** + * When other components requests the menu controller directly to show the menu, we must + * first fire off the request to the other listeners who will then propagate the call + * back to the controller with the right parameters. + */ + @Override + public void showMenu() { + mListeners.forEach(Listener::onPipShowMenu); + } + + /** + * Similar to {@link #showMenu(int, Rect, boolean, boolean, boolean)} but only show the menu + * upon PiP window transition is finished. + */ + public void showMenuWithPossibleDelay(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, + willResizeMenu /* withDelay=willResizeMenu here */, 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 (!maybeCreateSyncApplier()) { + return; + } + + mPipMenuView.showMenu(menuState, stackBounds, allowMenuTimeout, willResizeMenu, withDelay, + showResizeHandle); + } + + /** + * Move the PiP menu, which does a translation and possibly a scale transformation. + */ + @Override + public void movePipMenu(@Nullable SurfaceControl pipLeash, + @Nullable SurfaceControl.Transaction t, + Rect destinationBounds) { + if (destinationBounds.isEmpty()) { + return; + } + + if (!maybeCreateSyncApplier()) { + return; + } + + // If there is no pip leash supplied, that means the PiP leash is already finalized + // resizing and the PiP menu is also resized. We then want to do a scale from the current + // new menu bounds. + if (pipLeash != null && t != null) { + mPipMenuView.getBoundsOnScreen(mTmpSourceBounds); + } else { + mTmpSourceBounds.set(0, 0, destinationBounds.width(), destinationBounds.height()); + } + + mTmpSourceRectF.set(mTmpSourceBounds); + mTmpDestinationRectF.set(destinationBounds); + mMoveTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL); + SurfaceControl surfaceControl = getSurfaceControl(); + SurfaceParams params = new SurfaceParams.Builder(surfaceControl) + .withMatrix(mMoveTransform) + .build(); + if (pipLeash != null && t != null) { + SurfaceParams pipParams = new SurfaceParams.Builder(pipLeash) + .withMergeTransaction(t) + .build(); + mApplier.scheduleApply(params, pipParams); + } else { + mApplier.scheduleApply(params); + } + } + + /** + * Does an immediate window crop of the PiP menu. + */ + @Override + public void resizePipMenu(@Nullable SurfaceControl pipLeash, + @Nullable SurfaceControl.Transaction t, + Rect destinationBounds) { + if (destinationBounds.isEmpty()) { + return; + } + + if (!maybeCreateSyncApplier()) { + return; + } + + SurfaceControl surfaceControl = getSurfaceControl(); + SurfaceParams params = new SurfaceParams.Builder(surfaceControl) + .withWindowCrop(destinationBounds) + .build(); + if (pipLeash != null && t != null) { + SurfaceParams pipParams = new SurfaceParams.Builder(pipLeash) + .withMergeTransaction(t) + .build(); + mApplier.scheduleApply(params, pipParams); + } else { + mApplier.scheduleApply(params); + } + } + + private boolean maybeCreateSyncApplier() { + if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) { + Log.v(TAG, "Not going to move PiP, either menu or its parent is not created."); + return false; + } + + if (mApplier == null) { + mApplier = new SyncRtSurfaceTransactionApplier(mPipMenuView); + mPipMenuInputToken = mPipMenuView.getViewRootImpl().getInputToken(); + } + + return mApplier != null; + } + + /** + * 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 menu. + */ + @Override + public void setAppActions(ParceledListSlice<RemoteAction> appActions) { + mAppActions = appActions; + updateMenuActions(); + } + + void onPipExpand() { + mListeners.forEach(Listener::onPipExpand); + } + + void onPipDismiss() { + mListeners.forEach(Listener::onPipDismiss); + } + + /** + * @return the best set of actions to show in the PiP menu. + */ + private ParceledListSlice<RemoteAction> resolveMenuActions() { + if (isValidActions(mAppActions)) { + return mAppActions; + } + return mMediaActions; + } + + /** + * 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.addActionListener(mMediaActionListener); + } else { + // Once hidden, stop listening for media action changes. This call will trigger + // the menu actions to be updated again. + mMediaController.removeActionListener(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); + } + } + + 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/PipAccessibilityInteractionConnection.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java new file mode 100644 index 000000000000..3b65899364cd --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java @@ -0,0 +1,355 @@ +/* + * 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.RemoteException; +import android.view.MagnificationSpec; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityWindowInfo; +import android.view.accessibility.IAccessibilityInteractionConnection; +import android.view.accessibility.IAccessibilityInteractionConnectionCallback; + +import androidx.annotation.BinderThread; + +import com.android.wm.shell.R; +import com.android.wm.shell.common.ShellExecutor; +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 { + + public interface AccessibilityCallbacks { + void onAccessibilityShowMenu(); + } + + private static final long ACCESSIBILITY_NODE_ID = 1; + private List<AccessibilityNodeInfo> mAccessibilityNodeInfoList; + + private final Context mContext; + private final ShellExecutor mMainExcutor; + private final @NonNull PipBoundsState mPipBoundsState; + private final PipMotionHelper mMotionHelper; + private final PipTaskOrganizer mTaskOrganizer; + private final PipSnapAlgorithm mSnapAlgorithm; + private final Runnable mUpdateMovementBoundCallback; + private final AccessibilityCallbacks mCallbacks; + private final IAccessibilityInteractionConnection mConnectionImpl; + + 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, + ShellExecutor mainExcutor) { + mContext = context; + mMainExcutor = mainExcutor; + mPipBoundsState = pipBoundsState; + mMotionHelper = motionHelper; + mTaskOrganizer = taskOrganizer; + mSnapAlgorithm = snapAlgorithm; + mUpdateMovementBoundCallback = updateMovementBoundCallback; + mCallbacks = callbacks; + mConnectionImpl = new PipAccessibilityInteractionConnectionImpl(); + } + + public void register(AccessibilityManager am) { + am.setPictureInPictureActionReplacingConnection(mConnectionImpl); + } + + private 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 */ + } + } + + private 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 (mPipBoundsState.getBounds().width() == mNormalBounds.width() + && mPipBoundsState.getBounds().height() == mNormalBounds.height()) { + setToExpandedBounds(); + } else { + setToNormalBounds(); + } + result = true; + } else { + switch (action) { + case AccessibilityNodeInfo.ACTION_CLICK: + 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(mPipBoundsState.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(); + }); + } + + private 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 */ + } + } + + private 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 */ + } + } + + private 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 */ + } + } + + private 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 */ + } + } + + /** + * 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; + } + + @BinderThread + private class PipAccessibilityInteractionConnectionImpl + extends IAccessibilityInteractionConnection.Stub { + @Override + public void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, + Region bounds, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid, MagnificationSpec spec, + Bundle arguments) throws RemoteException { + mMainExcutor.execute(() -> { + PipAccessibilityInteractionConnection.this + .findAccessibilityNodeInfoByAccessibilityId(accessibilityNodeId, bounds, + interactionId, callback, flags, interrogatingPid, interrogatingTid, + spec, arguments); + }); + } + + @Override + public void findAccessibilityNodeInfosByViewId(long accessibilityNodeId, String viewId, + Region bounds, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid, MagnificationSpec spec) + throws RemoteException { + mMainExcutor.execute(() -> { + PipAccessibilityInteractionConnection.this.findAccessibilityNodeInfosByViewId( + accessibilityNodeId, viewId, bounds, interactionId, callback, flags, + interrogatingPid, interrogatingTid, spec); + }); + } + + @Override + public void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, + Region bounds, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid, MagnificationSpec spec) + throws RemoteException { + mMainExcutor.execute(() -> { + PipAccessibilityInteractionConnection.this.findAccessibilityNodeInfosByText( + accessibilityNodeId, text, bounds, interactionId, callback, flags, + interrogatingPid, interrogatingTid, spec); + }); + } + + @Override + public void findFocus(long accessibilityNodeId, int focusType, Region bounds, + int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid, MagnificationSpec spec) + throws RemoteException { + mMainExcutor.execute(() -> { + PipAccessibilityInteractionConnection.this.findFocus(accessibilityNodeId, focusType, + bounds, interactionId, callback, flags, interrogatingPid, interrogatingTid, + spec); + }); + } + + @Override + public void focusSearch(long accessibilityNodeId, int direction, Region bounds, + int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid, MagnificationSpec spec) + throws RemoteException { + mMainExcutor.execute(() -> { + PipAccessibilityInteractionConnection.this.focusSearch(accessibilityNodeId, + direction, + bounds, interactionId, callback, flags, interrogatingPid, interrogatingTid, + spec); + }); + } + + @Override + public void performAccessibilityAction(long accessibilityNodeId, int action, + Bundle arguments, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int flags, + int interrogatingPid, long interrogatingTid) throws RemoteException { + mMainExcutor.execute(() -> { + PipAccessibilityInteractionConnection.this.performAccessibilityAction( + accessibilityNodeId, action, arguments, interactionId, callback, flags, + interrogatingPid, interrogatingTid); + }); + } + + @Override + public void clearAccessibilityFocus() throws RemoteException { + // Do nothing + } + + @Override + public void notifyOutsideTouch() throws RemoteException { + // Do nothing + } + } +} 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..d97d2d6ebb4f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAppOpsListener.java @@ -0,0 +1,95 @@ +/* + * 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.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.util.Pair; + +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.pip.PipUtils; + +public class PipAppOpsListener { + private static final String TAG = PipAppOpsListener.class.getSimpleName(); + + private Context mContext; + private ShellExecutor mMainExecutor; + 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); + 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) { + mMainExecutor.execute(() -> mCallback.dismissPip()); + } + } + } catch (NameNotFoundException e) { + // Unregister the listener if the package can't be found + unregisterAppOpsListener(); + } + } + }; + + public PipAppOpsListener(Context context, Callback callback, ShellExecutor mainExecutor) { + mContext = context; + mMainExecutor = mainExecutor; + 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..c06f9d03cdf7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -0,0 +1,744 @@ +/* + * 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 static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; +import static android.view.WindowManager.INPUT_CONSUMER_PIP; + +import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; + +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +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.content.res.Configuration; +import android.graphics.Rect; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Log; +import android.util.Pair; +import android.util.Size; +import android.util.Slog; +import android.view.DisplayInfo; +import android.view.WindowManagerGlobal; +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.common.DisplayLayout; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TaskStackListenerCallback; +import com.android.wm.shell.common.TaskStackListenerImpl; +import com.android.wm.shell.pip.PinnedStackListenerForwarder; +import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.pip.PipBoundsAlgorithm; +import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipMediaController; +import com.android.wm.shell.pip.PipSnapAlgorithm; +import com.android.wm.shell.pip.PipTaskOrganizer; +import com.android.wm.shell.pip.PipUtils; + +import java.io.PrintWriter; +import java.util.function.Consumer; + +/** + * Manages the picture-in-picture (PIP) UI and states for Phones. + */ +public class PipController implements PipTaskOrganizer.PipTransitionCallback { + private static final String TAG = "PipController"; + + private Context mContext; + protected ShellExecutor mMainExecutor; + private DisplayController mDisplayController; + private PipInputConsumer mPipInputConsumer; + private WindowManagerShellWrapper mWindowManagerShellWrapper; + private PipAppOpsListener mAppOpsListener; + private PipMediaController mMediaController; + private PipBoundsAlgorithm mPipBoundsAlgorithm; + private PipBoundsState mPipBoundsState; + private PipTouchHandler mTouchHandler; + protected final PipImpl mImpl = new PipImpl(); + + private final Rect mTmpInsetBounds = new Rect(); + + private boolean mIsInFixedRotation; + private Consumer<Boolean> mPinnedStackAnimationRecentsCallback; + + protected PhonePipMenuController mMenuController; + protected PipTaskOrganizer mPipTaskOrganizer; + protected PinnedStackListenerForwarder.PinnedStackListener mPinnedStackListener = + new PipControllerPinnedStackListener(); + + /** + * 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. + onDisplayRotationChangedNotInPip(mContext, toRotation); + // do not forget to update the movement bounds as well. + updateMovementBounds(mPipBoundsState.getNormalBounds(), true /* fromRotation */, + false /* fromImeAdjustment */, false /* fromShelfAdjustment */, t); + 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 Rect outBounds = new Rect(); + final boolean changed = onDisplayRotationChanged(mContext, outBounds, 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(outBounds, 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) { + // Update the shelf visibility without updating the movement bounds. We're already + // updating them below with the |fromRotation| flag set, which is more accurate + // than using the |fromShelfAdjustment|. + mPipBoundsState.setShelfVisibility(false /* showing */, 0 /* height */, + false /* updateMovementBounds */); + mPipBoundsState.setImeVisibility(false /* showing */, 0 /* height */); + mTouchHandler.onShelfVisibilityChanged(false, 0); + mTouchHandler.onImeVisibilityChanged(false, 0); + } + + updateMovementBounds(outBounds, true /* fromRotation */, false /* fromImeAdjustment */, + false /* fromShelfAdjustment */, t); + } + }; + + private final DisplayController.OnDisplaysChangedListener mDisplaysChangedListener = + 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) { + if (displayId != mPipBoundsState.getDisplayId()) { + return; + } + onDisplayChanged(mDisplayController.getDisplayLayout(displayId), + false /* saveRestoreSnapFraction */); + } + + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + if (displayId != mPipBoundsState.getDisplayId()) { + return; + } + onDisplayChanged(mDisplayController.getDisplayLayout(displayId), + true /* saveRestoreSnapFraction */); + } + }; + + /** + * Handler for messages from the PIP controller. + */ + private class PipControllerPinnedStackListener extends + PinnedStackListenerForwarder.PinnedStackListener { + @Override + public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { + mPipBoundsState.setImeVisibility(imeVisible, imeHeight); + mTouchHandler.onImeVisibilityChanged(imeVisible, imeHeight); + } + + @Override + public void onMovementBoundsChanged(boolean fromImeAdjustment) { + updateMovementBounds(null /* toBounds */, + false /* fromRotation */, fromImeAdjustment, false /* fromShelfAdjustment */, + null /* windowContainerTransaction */); + } + + @Override + public void onActionsChanged(ParceledListSlice<RemoteAction> actions) { + mMenuController.setAppActions(actions); + } + + @Override + public void onActivityHidden(ComponentName componentName) { + 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 onAspectRatioChanged(float aspectRatio) { + // TODO(b/169373982): Remove this callback as it is redundant with PipTaskOrg params + // change. + mPipBoundsState.setAspectRatio(aspectRatio); + mTouchHandler.onAspectRatioChanged(); + } + } + + + /** + * Instantiates {@link PipController}, returns {@code null} if the feature not supported. + */ + @Nullable + public static Pip create(Context context, DisplayController displayController, + PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm, + PipBoundsState pipBoundsState, PipMediaController pipMediaController, + PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer, + PipTouchHandler pipTouchHandler, WindowManagerShellWrapper windowManagerShellWrapper, + TaskStackListenerImpl taskStackListener, ShellExecutor mainExecutor) { + 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, pipBoundsAlgorithm, + pipBoundsState, pipMediaController, phonePipMenuController, pipTaskOrganizer, + pipTouchHandler, windowManagerShellWrapper, taskStackListener, mainExecutor) + .mImpl; + } + + protected PipController(Context context, + DisplayController displayController, + PipAppOpsListener pipAppOpsListener, + PipBoundsAlgorithm pipBoundsAlgorithm, + @NonNull PipBoundsState pipBoundsState, + PipMediaController pipMediaController, + PhonePipMenuController phonePipMenuController, + PipTaskOrganizer pipTaskOrganizer, + PipTouchHandler pipTouchHandler, + WindowManagerShellWrapper windowManagerShellWrapper, + TaskStackListenerImpl taskStackListener, + ShellExecutor mainExecutor + ) { + // 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; + mPipBoundsAlgorithm = pipBoundsAlgorithm; + mPipBoundsState = pipBoundsState; + mPipTaskOrganizer = pipTaskOrganizer; + mMainExecutor = mainExecutor; + mMediaController = pipMediaController; + mMenuController = phonePipMenuController; + mTouchHandler = pipTouchHandler; + mAppOpsListener = pipAppOpsListener; + mPipInputConsumer = new PipInputConsumer(WindowManagerGlobal.getWindowManagerService(), + INPUT_CONSUMER_PIP, mainExecutor); + mPipTaskOrganizer.registerPipTransitionCallback(this); + mPipTaskOrganizer.registerOnDisplayIdChangeCallback((int displayId) -> { + mPipBoundsState.setDisplayId(displayId); + onDisplayChanged(displayController.getDisplayLayout(displayId), + false /* saveRestoreSnapFraction */); + }); + mPipBoundsState.setOnMinimalSizeChangeCallback( + () -> { + // The minimal size drives the normal bounds, so they need to be recalculated. + updateMovementBounds(null /* toBounds */, false /* fromRotation */, + false /* fromImeAdjustment */, false /* fromShelfAdjustment */, + null /* wct */); + }); + mPipBoundsState.setOnShelfVisibilityChangeCallback( + (isShowing, height, updateMovementBounds) -> { + mTouchHandler.onShelfVisibilityChanged(isShowing, height); + if (updateMovementBounds) { + updateMovementBounds(mPipBoundsState.getBounds(), + false /* fromRotation */, false /* fromImeAdjustment */, + true /* fromShelfAdjustment */, + null /* windowContainerTransaction */); + } + }); + if (mTouchHandler != null) { + // Register the listener for input consumer touch events. Only for Phone + mPipInputConsumer.setInputListener(mTouchHandler::handleTouchEvent); + mPipInputConsumer.setRegistrationListener(mTouchHandler::onRegistrationChanged); + } + displayController.addDisplayChangingController(mRotationController); + displayController.addDisplayWindowListener(mDisplaysChangedListener); + + // Ensure that we have the display info in case we get calls to update the bounds before the + // listener calls back + mPipBoundsState.setDisplayId(context.getDisplayId()); + mPipBoundsState.setDisplayLayout(new DisplayLayout(context, context.getDisplay())); + + try { + mWindowManagerShellWrapper.addPinnedStackListener(mPinnedStackListener); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to register pinned stack listener", e); + } + + try { + ActivityTaskManager.RootTaskInfo taskInfo = ActivityTaskManager.getService() + .getRootTaskInfo(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); + if (taskInfo != null) { + // If SystemUI restart, and it already existed a pinned stack, + // register the pip input consumer to ensure touch can send to it. + mPipInputConsumer.registerInputConsumer(); + } + } catch (RemoteException | UnsupportedOperationException e) { + Log.e(TAG, "Failed to register pinned stack listener", e); + e.printStackTrace(); + } + + // Handle for system task stack changes. + taskStackListener.addListener( + new TaskStackListenerCallback() { + @Override + public void onActivityPinned(String packageName, int userId, int taskId, + int stackId) { + mTouchHandler.onActivityPinned(); + mMediaController.onActivityPinned(); + mAppOpsListener.onActivityPinned(packageName); + mPipInputConsumer.registerInputConsumer(); + } + + @Override + public void onActivityUnpinned() { + final Pair<ComponentName, Integer> topPipActivityInfo = + PipUtils.getTopPipActivity(mContext); + final ComponentName topActivity = topPipActivityInfo.first; + mTouchHandler.onActivityUnpinned(topActivity); + mAppOpsListener.onActivityUnpinned(); + mPipInputConsumer.unregisterInputConsumer(); + } + + @Override + public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, + boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { + if (task.getWindowingMode() != WINDOWING_MODE_PINNED) { + return; + } + mTouchHandler.getMotionHelper().expandLeavePip( + clearedTask /* skipAnimation */); + } + }); + } + + private void onConfigurationChanged(Configuration newConfig) { + mPipBoundsAlgorithm.onConfigurationChanged(mContext); + mTouchHandler.onConfigurationChanged(); + mPipBoundsState.onConfigurationChanged(); + } + + private void onDensityOrFontScaleChanged() { + mPipTaskOrganizer.onDensityOrFontScaleChanged(mContext); + } + + private void onOverlayChanged() { + onDisplayChanged(new DisplayLayout(mContext, mContext.getDisplay()), + false /* saveRestoreSnapFraction */); + } + + private void onDisplayChanged(DisplayLayout layout, boolean saveRestoreSnapFraction) { + Runnable updateDisplayLayout = () -> { + mPipBoundsState.setDisplayLayout(layout); + updateMovementBounds(null /* toBounds */, + false /* fromRotation */, false /* fromImeAdjustment */, + false /* fromShelfAdjustment */, + null /* windowContainerTransaction */); + }; + + if (saveRestoreSnapFraction) { + // Calculate the snap fraction of the current stack along the old movement bounds + final PipSnapAlgorithm pipSnapAlgorithm = mPipBoundsAlgorithm.getSnapAlgorithm(); + final Rect postChangeStackBounds = new Rect(mPipBoundsState.getBounds()); + final float snapFraction = pipSnapAlgorithm.getSnapFraction(postChangeStackBounds, + mPipBoundsAlgorithm.getMovementBounds(postChangeStackBounds), + mPipBoundsState.getStashedState()); + + updateDisplayLayout.run(); + + // Calculate the stack bounds in the new orientation based on same fraction along the + // rotated movement bounds. + final Rect postChangeMovementBounds = mPipBoundsAlgorithm.getMovementBounds( + postChangeStackBounds, false /* adjustForIme */); + pipSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds, + snapFraction, mPipBoundsState.getStashedState(), + mPipBoundsState.getStashOffset(), + mPipBoundsState.getDisplayBounds()); + + mTouchHandler.getMotionHelper().movePip(postChangeStackBounds); + } else { + updateDisplayLayout.run(); + } + } + + private void registerSessionListenerForCurrentUser() { + mMediaController.registerSessionListenerForCurrentUser(); + } + + private void onSystemUiStateChanged(boolean isValidState, int flag) { + mTouchHandler.onSystemUiStateChanged(isValidState); + } + + /** + * Expands the PIP. + */ + public void expandPip() { + mTouchHandler.getMotionHelper().expandLeavePip(false /* skipAnimation */); + } + + private PipTouchHandler getPipTouchHandler() { + return mTouchHandler; + } + + /** + * Hides the PIP menu. + */ + 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. + */ + private void setShelfHeight(boolean visible, int height) { + setShelfHeightLocked(visible, height); + } + + private void setShelfHeightLocked(boolean visible, int height) { + final int shelfHeight = visible ? height : 0; + mPipBoundsState.setShelfVisibility(visible, shelfHeight); + } + + private void setPinnedStackAnimationType(int animationType) { + mPipTaskOrganizer.setOneShotAnimationType(animationType); + } + + private void setPinnedStackAnimationListener(Consumer<Boolean> callback) { + mPinnedStackAnimationRecentsCallback = callback; + } + + private Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, + PictureInPictureParams pictureInPictureParams, + int launcherRotation, int shelfHeight) { + setShelfHeightLocked(shelfHeight > 0 /* visible */, shelfHeight); + onDisplayRotationChangedNotInPip(mContext, launcherRotation); + return mPipTaskOrganizer.startSwipePipToHome(componentName, activityInfo, + pictureInPictureParams); + } + + private void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds) { + mPipTaskOrganizer.stopSwipePipToHome(componentName, destinationBounds); + } + + /** + * Set a listener to watch out for PiP bounds. This is mostly used by SystemUI's + * Back-gesture handler, to avoid conflicting with PiP when it's stashed. + */ + private void setPipExclusionBoundsChangeListener( + Consumer<Rect> pipExclusionBoundsChangeListener) { + mTouchHandler.setPipExclusionBoundsChangeListener(pipExclusionBoundsChangeListener); + } + + @Override + public void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds) { + if (isOutPipDirection(direction)) { + // Exiting PIP, save the reentry state to restore to when re-entering. + saveReentryState(pipBounds); + } + // Disable touches while the animation is running + mTouchHandler.setTouchEnabled(false); + if (mPinnedStackAnimationRecentsCallback != null) { + mPinnedStackAnimationRecentsCallback.accept(true); + } + } + + /** Save the state to restore to on re-entry. */ + public void saveReentryState(Rect pipBounds) { + float snapFraction = mPipBoundsAlgorithm.getSnapFraction(pipBounds); + if (mPipBoundsState.hasUserResizedPip()) { + final Rect reentryBounds = mTouchHandler.getUserResizeBounds(); + final Size reentrySize = new Size(reentryBounds.width(), reentryBounds.height()); + mPipBoundsState.saveReentryState(reentrySize, snapFraction); + } else { + mPipBoundsState.saveReentryState(null /* bounds */, snapFraction); + } + } + + @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); + final int rotation = mPipBoundsState.getDisplayLayout().rotation(); + + mPipBoundsAlgorithm.getInsetBounds(mTmpInsetBounds); + mPipBoundsState.setNormalBounds(mPipBoundsAlgorithm.getNormalBounds()); + if (outBounds.isEmpty()) { + outBounds.set(mPipBoundsAlgorithm.getDefaultBounds()); + } + + // mTouchHandler would rely on the bounds populated from mPipTaskOrganizer + mPipTaskOrganizer.onMovementBoundsChanged(outBounds, fromRotation, fromImeAdjustment, + fromShelfAdjustment, wct); + mTouchHandler.onMovementBoundsChanged(mTmpInsetBounds, mPipBoundsState.getNormalBounds(), + outBounds, fromImeAdjustment, fromShelfAdjustment, rotation); + } + + /** + * Updates 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. + */ + private 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 + mPipBoundsState.getDisplayLayout().rotateTo(context.getResources(), toRotation); + } + + /** + * 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. + */ + private 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 display + if ((displayId != mPipBoundsState.getDisplayId()) || (fromRotation == toRotation)) { + return false; + } + + // Bail early if the pinned task is staled. + final ActivityTaskManager.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; + } + final PipSnapAlgorithm pipSnapAlgorithm = mPipBoundsAlgorithm.getSnapAlgorithm(); + + // Calculate the snap fraction of the current stack along the old movement bounds + final Rect postChangeStackBounds = new Rect(oldBounds); + final float snapFraction = pipSnapAlgorithm.getSnapFraction(postChangeStackBounds, + mPipBoundsAlgorithm.getMovementBounds(postChangeStackBounds), + mPipBoundsState.getStashedState()); + + // Update the display layout + mPipBoundsState.getDisplayLayout().rotateTo(context.getResources(), toRotation); + + // Calculate the stack bounds in the new orientation based on same fraction along the + // rotated movement bounds. + final Rect postChangeMovementBounds = mPipBoundsAlgorithm.getMovementBounds( + postChangeStackBounds, false /* adjustForIme */); + pipSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds, + snapFraction, mPipBoundsState.getStashedState(), mPipBoundsState.getStashOffset(), + mPipBoundsState.getDisplayBounds()); + + mPipBoundsAlgorithm.getInsetBounds(outInsetBounds); + outBounds.set(postChangeStackBounds); + t.setBounds(pinnedTaskInfo.token, outBounds); + return true; + } + + private void dump(PrintWriter pw) { + final String innerPrefix = " "; + pw.println(TAG); + mMenuController.dump(pw, innerPrefix); + mTouchHandler.dump(pw, innerPrefix); + mPipBoundsAlgorithm.dump(pw, innerPrefix); + mPipTaskOrganizer.dump(pw, innerPrefix); + mPipBoundsState.dump(pw, innerPrefix); + mPipInputConsumer.dump(pw, innerPrefix); + } + + private class PipImpl implements Pip { + @Override + public void hidePipMenu(Runnable onStartCallback, Runnable onEndCallback) { + mMainExecutor.execute(() -> { + PipController.this.hidePipMenu(onStartCallback, onEndCallback); + }); + } + + @Override + public void expandPip() { + mMainExecutor.execute(() -> { + PipController.this.expandPip(); + }); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + mMainExecutor.execute(() -> { + PipController.this.onConfigurationChanged(newConfig); + }); + } + + @Override + public void onDensityOrFontScaleChanged() { + mMainExecutor.execute(() -> { + PipController.this.onDensityOrFontScaleChanged(); + }); + } + + @Override + public void onOverlayChanged() { + mMainExecutor.execute(() -> { + PipController.this.onOverlayChanged(); + }); + } + + @Override + public void onSystemUiStateChanged(boolean isSysUiStateValid, int flag) { + mMainExecutor.execute(() -> { + PipController.this.onSystemUiStateChanged(isSysUiStateValid, flag); + }); + } + + @Override + public void registerSessionListenerForCurrentUser() { + mMainExecutor.execute(() -> { + PipController.this.registerSessionListenerForCurrentUser(); + }); + } + + @Override + public void setShelfHeight(boolean visible, int height) { + mMainExecutor.execute(() -> { + PipController.this.setShelfHeight(visible, height); + }); + } + + @Override + public void setPinnedStackAnimationListener(Consumer<Boolean> callback) { + mMainExecutor.execute(() -> { + PipController.this.setPinnedStackAnimationListener(callback); + }); + } + + @Override + public void setPinnedStackAnimationType(int animationType) { + mMainExecutor.execute(() -> { + PipController.this.setPinnedStackAnimationType(animationType); + }); + } + + @Override + public void setPipExclusionBoundsChangeListener(Consumer<Rect> listener) { + mMainExecutor.execute(() -> { + PipController.this.setPipExclusionBoundsChangeListener(listener); + }); + } + + @Override + public void showPictureInPictureMenu() { + mMainExecutor.execute(() -> { + PipController.this.showPictureInPictureMenu(); + }); + } + + @Override + public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, + PictureInPictureParams pictureInPictureParams, int launcherRotation, + int shelfHeight) { + Rect[] result = new Rect[1]; + try { + mMainExecutor.executeBlocking(() -> { + result[0] = PipController.this.startSwipePipToHome(componentName, activityInfo, + pictureInPictureParams, launcherRotation, shelfHeight); + }); + } catch (InterruptedException e) { + Slog.e(TAG, "Failed to start swipe pip to home"); + } + return result[0]; + } + + @Override + public void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds) { + mMainExecutor.execute(() -> { + PipController.this.stopSwipePipToHome(componentName, destinationBounds); + }); + } + + @Override + public void dump(PrintWriter pw) { + try { + mMainExecutor.executeBlocking(() -> { + PipController.this.dump(pw); + }); + } catch (InterruptedException e) { + Slog.e(TAG, "Failed to dump PipController in 2s"); + } + } + } +} 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..d9a7bdb2eca6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java @@ -0,0 +1,292 @@ +/* + * 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.ShellExecutor; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; +import com.android.wm.shell.pip.PipUiEventLogger; + +import java.util.concurrent.TimeUnit; + +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 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 ViewGroup mTargetViewContainer; + + /** Circle view used to render the dismiss target. */ + private DismissCircleView mTargetView; + + /** + * MagneticTarget instance wrapping the target view and allowing us to set its magnetic radius. + */ + private MagnetizedObject.MagneticTarget mMagneticTarget; + + /** + * PhysicsAnimator instance for animating the dismiss target in/out. + */ + private 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); + + // 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 ShellExecutor mMainExecutor; + + public PipDismissTargetHandler(Context context, PipUiEventLogger pipUiEventLogger, + PipMotionHelper motionHelper, ShellExecutor mainExecutor) { + mContext = context; + mPipUiEventLogger = pipUiEventLogger; + mMotionHelper = motionHelper; + mMainExecutor = mainExecutor; + 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); + + mMainExecutor.execute(() -> { + 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) { + mMainExecutor.executeDelayed(() -> { + mMotionHelper.notifyDismissalPending(); + mMotionHelper.animateDismiss(); + hideDismissTargetMaybe(); + + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_DRAG_TO_REMOVE); + }, 0); + } + }); + + 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()) { + 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; + } + + 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() { + if (mTargetViewContainer.isAttachedToWindow()) { + mWindowManager.removeViewImmediate(mTargetViewContainer); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java new file mode 100644 index 000000000000..6e3a20d5f2b2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java @@ -0,0 +1,184 @@ +/* + * 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.view.Display.DEFAULT_DISPLAY; + +import android.os.Binder; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.util.Log; +import android.view.BatchedInputEventReceiver; +import android.view.Choreographer; +import android.view.IWindowManager; +import android.view.InputChannel; +import android.view.InputEvent; + +import com.android.wm.shell.common.ShellExecutor; + +import java.io.PrintWriter; + +/** + * Manages the input consumer that allows the Shell to directly receive input. + */ +public class PipInputConsumer { + + private static final String TAG = PipInputConsumer.class.getSimpleName(); + + /** + * Listener interface for callers to subscribe to input events. + */ + public interface InputListener { + /** Handles any input event. */ + boolean onInputEvent(InputEvent ev); + } + + /** + * Listener interface for callers to learn when this class is registered or unregistered with + * window manager + */ + public interface RegistrationListener { + void onRegistrationChanged(boolean isRegistered); + } + + /** + * Input handler used for the input consumer. Input events are batched and consumed with the + * SurfaceFlinger vsync. + */ + private final class InputEventReceiver extends BatchedInputEventReceiver { + + InputEventReceiver(InputChannel inputChannel, Looper looper, + Choreographer choreographer) { + super(inputChannel, looper, choreographer); + } + + @Override + public void onInputEvent(InputEvent event) { + boolean handled = true; + try { + if (mListener != null) { + handled = mListener.onInputEvent(event); + } + } finally { + finishInputEvent(event, handled); + } + } + } + + private final IWindowManager mWindowManager; + private final IBinder mToken; + private final String mName; + private final ShellExecutor mMainExecutor; + + private InputEventReceiver mInputEventReceiver; + private InputListener mListener; + private RegistrationListener mRegistrationListener; + + /** + * @param name the name corresponding to the input consumer that is defined in the system. + */ + public PipInputConsumer(IWindowManager windowManager, String name, + ShellExecutor mainExecutor) { + mWindowManager = windowManager; + mToken = new Binder(); + mName = name; + mMainExecutor = mainExecutor; + } + + /** + * Sets the input listener. + */ + public void setInputListener(InputListener listener) { + mListener = listener; + } + + /** + * Sets the registration listener. + */ + public void setRegistrationListener(RegistrationListener listener) { + mRegistrationListener = listener; + mMainExecutor.execute(() -> { + if (mRegistrationListener != null) { + mRegistrationListener.onRegistrationChanged(mInputEventReceiver != null); + } + }); + } + + /** + * Check if the InputConsumer is currently registered with WindowManager + * + * @return {@code true} if registered, {@code false} if not. + */ + public boolean isRegistered() { + return mInputEventReceiver != null; + } + + /** + * Registers the input consumer. + */ + public void registerInputConsumer() { + if (mInputEventReceiver != null) { + return; + } + final InputChannel inputChannel = new InputChannel(); + try { + // TODO(b/113087003): Support Picture-in-picture in multi-display. + mWindowManager.destroyInputConsumer(mName, DEFAULT_DISPLAY); + mWindowManager.createInputConsumer(mToken, mName, DEFAULT_DISPLAY, inputChannel); + } catch (RemoteException e) { + Log.e(TAG, "Failed to create input consumer", e); + } + mMainExecutor.execute(() -> { + // Choreographer.getSfInstance() must be called on the thread that the input event + // receiver should be receiving events + mInputEventReceiver = new InputEventReceiver(inputChannel, + Looper.myLooper(), Choreographer.getSfInstance()); + if (mRegistrationListener != null) { + mRegistrationListener.onRegistrationChanged(true /* isRegistered */); + } + }); + } + + /** + * Unregisters the input consumer. + */ + public void unregisterInputConsumer() { + if (mInputEventReceiver == null) { + return; + } + try { + // TODO(b/113087003): Support Picture-in-picture in multi-display. + mWindowManager.destroyInputConsumer(mName, DEFAULT_DISPLAY); + } catch (RemoteException e) { + Log.e(TAG, "Failed to destroy input consumer", e); + } + mInputEventReceiver.dispose(); + mInputEventReceiver = null; + mMainExecutor.execute(() -> { + if (mRegistrationListener != null) { + mRegistrationListener.onRegistrationChanged(false /* isRegistered */); + } + }); + } + + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "registered=" + (mInputEventReceiver != null)); + } +} 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..6d12752d9218 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuIconsAlgorithm.java @@ -0,0 +1,87 @@ +/* + * 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."); + return; + } + + //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..962c4672644a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java @@ -0,0 +1,506 @@ +/* + * 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.PhonePipMenuController.MENU_STATE_CLOSE; +import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL; +import static com.android.wm.shell.pip.phone.PhonePipMenuController.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.annotation.Nullable; +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.util.Size; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.SurfaceControl; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewRootImpl; +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 com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.pip.PipUtils; + +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 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 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 PhonePipMenuController 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 ShellExecutor mMainExecutor; + private Handler mMainHandler; + + 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, PhonePipMenuController controller, + ShellExecutor mainExecutor, Handler mainHandler) { + super(context, null, 0); + mContext = context; + mController = controller; + mMainExecutor = mainExecutor; + mMainHandler = mainHandler; + + 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.showMenu(); + } + 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(); + 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); + setVisibility(VISIBLE); + mMenuContainerAnimator.start(); + }); + } else { + notifyMenuStateChange(menuState, resizeMenuOnShow, null); + setVisibility(VISIBLE); + mMenuContainerAnimator.start(); + } + updateActionViews(stackBounds); + } 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); + } + } + } + + @Nullable SurfaceControl getWindowSurfaceControl() { + final ViewRootImpl root = getViewRootImpl(); + if (root == null) { + return null; + } + final SurfaceControl out = root.getSurfaceControl(); + if (out != null && out.isValid()) { + return out; + } + return null; + } + + /** + * 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) { + setVisibility(GONE); + if (animationFinishedRunnable != null) { + animationFinishedRunnable.run(); + } + } + }); + mMenuContainerAnimator.start(); + } + } + + /** + * @return Estimated minimum {@link Size} to hold the actions. + * See also {@link #updateActionViews(Rect)} + */ + Size getEstimatedMinMenuSize() { + final int pipActionSize = getResources().getDimensionPixelSize(R.dimen.pip_action_size); + // the minimum width would be (2 * pipActionSize) since we have settings and dismiss button + // on the top action container. + final int width = Math.max(2, mActions.size()) * pipActionSize; + final int height = getResources().getDimensionPixelSize(R.dimen.pip_expand_action_size) + + getResources().getDimensionPixelSize(R.dimen.pip_action_padding) + + getResources().getDimensionPixelSize(R.dimen.pip_expand_container_edge_margin); + return new Size(width, height); + } + + 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 || mMenuState == MENU_STATE_NONE) { + 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); + } + }, mMainHandler); + actionView.setContentDescription(action.getContentDescription()); + if (action.isEnabled()) { + actionView.setOnClickListener(v -> { + 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); + 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.of(topPipActivityInfo.second)); + } + } + + private void cancelDelayedHide() { + mMainExecutor.removeCallbacks(mHideMenuRunnable); + } + + private void repostDelayedHide(int delay) { + int recommendedTimeout = mAccessibilityManager.getRecommendedTimeoutMillis(delay, + FLAG_CONTENT_ICONS | FLAG_CONTENT_CONTROLS); + mMainExecutor.removeCallbacks(mHideMenuRunnable); + mMainExecutor.executeDelayed(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..fd4ea61713ef --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java @@ -0,0 +1,667 @@ +/* + * 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.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND; +import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_LEFT; +import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT; + +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.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.ShellExecutor; +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.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 UNSTASH_DURATION = 250; + private static final int LEAVE_PIP_DURATION = 300; + private static final int SHIFT_DURATION = 300; + + /** 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 @NonNull PipBoundsState mPipBoundsState; + + private PhonePipMenuController mMenuController; + private PipSnapAlgorithm mSnapAlgorithm; + + /** The region that all of PIP must stay within. */ + private final Rect mFloatingAllowedArea = new Rect(); + + /** Coordinator instance for resolving conflicts with other floating content. */ + private FloatingContentCoordinator mFloatingContentCoordinator; + + private ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal = + ThreadLocal.withInitial(() -> { + final Looper initialLooper = Looper.myLooper(); + final FrameCallbackScheduler scheduler = new FrameCallbackScheduler() { + @Override + public void postFrameCallback(@androidx.annotation.NonNull Runnable runnable) { + Choreographer.getSfInstance().postFrameCallback(t -> runnable.run()); + } + + @Override + public boolean isCurrentThread() { + return Looper.myLooper() == initialLooper; + } + }; + AnimationHandler handler = new AnimationHandler(scheduler); + return handler; + }); + + /** + * PhysicsAnimator instance for animating {@link PipBoundsState#getMotionBoundsState()} + * using physics animations. + */ + private final PhysicsAnimator<Rect> mTemporaryBoundsPhysicsAnimator; + + private MagnetizedObject<Rect> mMagnetizedPip; + + /** + * Update listener that resizes the PIP to {@link PipBoundsState#getMotionBoundsState()}. + */ + 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) -> { + mMenuController.updateMenuLayout(newBounds); + mPipBoundsState.setBounds(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, PhonePipMenuController menuController, + PipSnapAlgorithm snapAlgorithm, FloatingContentCoordinator floatingContentCoordinator, + ShellExecutor mainExecutor) { + mContext = context; + mPipTaskOrganizer = pipTaskOrganizer; + mPipBoundsState = pipBoundsState; + mMenuController = menuController; + mSnapAlgorithm = snapAlgorithm; + mFloatingContentCoordinator = floatingContentCoordinator; + mPipTaskOrganizer.registerPipTransitionCallback(mPipTransitionCallback); + mTemporaryBoundsPhysicsAnimator = PhysicsAnimator.getInstance( + mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); + + // Need to get the shell main thread sf vsync animation handler + mainExecutor.execute(() -> { + mTemporaryBoundsPhysicsAnimator.setCustomAnimationHandler( + mSfAnimationHandlerThreadLocal.get()); + }); + + mResizePipUpdateListener = (target, values) -> { + if (mPipBoundsState.getMotionBoundsState().isInMotion()) { + mPipTaskOrganizer.scheduleUserResizePip(getBounds(), + mPipBoundsState.getMotionBoundsState().getBoundsInMotion(), null); + } + }; + } + + @NonNull + @Override + public Rect getFloatingBoundsOnScreen() { + return !mPipBoundsState.getMotionBoundsState().getAnimatingToBounds().isEmpty() + ? mPipBoundsState.getMotionBoundsState().getAnimatingToBounds() : getBounds(); + } + + @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() { + cancelPhysicsAnimation(); + mPipBoundsState.getMotionBoundsState().onAllAnimationsEnded(); + + if (mPipTaskOrganizer.isInPip()) { + mFloatingContentCoordinator.onContentMoved(this); + } + } + + /** + * 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. + cancelPhysicsAnimation(); + + if (!isDragging) { + resizePipUnchecked(toBounds); + mPipBoundsState.setBounds(toBounds); + } else { + mPipBoundsState.getMotionBoundsState().setBoundsInMotion(toBounds); + mPipTaskOrganizer.scheduleUserResizePip(getBounds(), toBounds, + (Rect newBounds) -> { + 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, getBounds().width(), mSpringConfig) + .spring(FloatProperties.RECT_HEIGHT, getBounds().height(), mSpringConfig) + .spring(FloatProperties.RECT_X, toBounds.left, mSpringConfig) + .spring(FloatProperties.RECT_Y, toBounds.top, mSpringConfig); + + startBoundsAnimator(toBounds.left /* toX */, toBounds.top /* toY */); + } + } + + /** 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 = getBounds().width() / 2; + final float desiredHeight = getBounds().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 (!mPipBoundsState.getMotionBoundsState().isInMotion()) { + mPipBoundsState.getMotionBoundsState().setBoundsInMotion(getBounds()); + } + 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); + } + + /** 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, " ")); + } + cancelPhysicsAnimation(); + mMenuController.hideMenuWithoutResize(); + 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, " ")); + } + cancelPhysicsAnimation(); + mMenuController.hideMenuWithoutResize(); + mPipTaskOrganizer.removePip(); + } + + /** Sets the movement bounds to use to constrain PIP position animations. */ + void onMovementBoundsChanged() { + 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(mPipBoundsState.getMovementBounds()); + mFloatingAllowedArea.right += getBounds().width(); + mFloatingAllowedArea.bottom += getBounds().height(); + } + + /** + * @return the PiP bounds. + */ + private Rect getBounds() { + return mPipBoundsState.getBounds(); + } + + /** + * Flings the PiP to the closest snap target. + */ + void flingToSnapTarget( + float velocityX, float velocityY, @Nullable Runnable postBoundsUpdateCallback) { + movetoTarget(velocityX, velocityY, postBoundsUpdateCallback, false /* isStash */); + } + + /** + * Stash PiP to the closest edge. We set velocityY to 0 to limit pure horizontal motion. + */ + void stashToEdge(float velocityX, @Nullable Runnable postBoundsUpdateCallback) { + mPipBoundsState.setStashed(velocityX < 0 ? STASH_TYPE_LEFT : STASH_TYPE_RIGHT); + movetoTarget(velocityX, 0 /* velocityY */, postBoundsUpdateCallback, true /* isStash */); + } + + private void movetoTarget( + float velocityX, + float velocityY, + @Nullable Runnable postBoundsUpdateCallback, + 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, getBounds().width(), mSpringConfig) + .spring(FloatProperties.RECT_HEIGHT, getBounds().height(), mSpringConfig) + .flingThenSpring( + FloatProperties.RECT_X, velocityX, + isStash ? mStashConfigX : mFlingConfigX, + mSpringConfig, true /* flingMustReachMinOrMax */) + .flingThenSpring( + FloatProperties.RECT_Y, velocityY, mFlingConfigY, mSpringConfig); + + final float leftEdge = isStash + ? mPipBoundsState.getStashOffset() - mPipBoundsState.getBounds().width() + : mPipBoundsState.getMovementBounds().left; + final float rightEdge = isStash + ? mPipBoundsState.getDisplayBounds().right - mPipBoundsState.getStashOffset() + : mPipBoundsState.getMovementBounds().right; + + final float xEndValue = velocityX < 0 ? leftEdge : rightEdge; + + final int startValueY = mPipBoundsState.getMotionBoundsState().getBoundsInMotion().top; + final float estimatedFlingYEndValue = + PhysicsAnimator.estimateFlingEndValue(startValueY, velocityY, mFlingConfigY); + + startBoundsAnimator(xEndValue /* toX */, estimatedFlingYEndValue /* toY */, + postBoundsUpdateCallback); + } + + /** + * 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. + mPipBoundsState.getMotionBoundsState().setBoundsInMotion(getBounds()); + } + + mTemporaryBoundsPhysicsAnimator + .spring(FloatProperties.RECT_X, bounds.left, springConfig) + .spring(FloatProperties.RECT_Y, bounds.top, springConfig); + startBoundsAnimator(bounds.left /* toX */, bounds.top /* toY */); + } + + /** + * 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, + mPipBoundsState.getMovementBounds().bottom + getBounds().height() * 2, + 0, + mSpringConfig) + .withEndActions(this::dismissPip); + + startBoundsAnimator( + getBounds().left /* toX */, getBounds().bottom + getBounds().height() /* toY */); + + 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(getBounds()), + 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(getBounds()), + currentMovementBounds, mPipBoundsState.getStashedState()); + } + + mSnapAlgorithm.applySnapFraction(normalBounds, normalMovementBounds, savedSnapFraction, + mPipBoundsState.getStashedState(), mPipBoundsState.getStashOffset(), + mPipBoundsState.getDisplayBounds()); + + if (immediate) { + movePip(normalBounds); + } else { + resizeAndAnimatePipUnchecked(normalBounds, SHRINK_STACK_FROM_MENU_DURATION); + } + } + + /** + * Animates the PiP from stashed state into un-stashed, popping it out from the edge. + */ + void animateToUnStashedBounds(Rect unstashedBounds) { + resizeAndAnimatePipUnchecked(unstashedBounds, UNSTASH_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, " ")); + } + cancelPhysicsAnimation(); + mPipTaskOrganizer.scheduleOffsetPip(originalBounds, offset, SHIFT_DURATION, + mUpdateBoundsCallback); + } + + /** + * Cancels all existing animations. + */ + private void cancelPhysicsAnimation() { + mTemporaryBoundsPhysicsAnimator.cancel(); + mPipBoundsState.getMotionBoundsState().onPhysicsAnimationEnded(); + mSpringingToTouch = false; + } + + /** Set new fling configs whose min/max values respect the given movement bounds. */ + private void rebuildFlingConfigs() { + mFlingConfigX = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION, + mPipBoundsState.getMovementBounds().left, + mPipBoundsState.getMovementBounds().right); + mFlingConfigY = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION, + mPipBoundsState.getMovementBounds().top, + mPipBoundsState.getMovementBounds().bottom); + mStashConfigX = new PhysicsAnimator.FlingConfig( + DEFAULT_FRICTION, + mPipBoundsState.getStashOffset() - mPipBoundsState.getBounds().width(), + mPipBoundsState.getDisplayBounds().right - mPipBoundsState.getStashOffset()); + } + + private void startBoundsAnimator(float toX, float toY) { + startBoundsAnimator(toX, toY, null /* postBoundsUpdateCallback */); + } + + /** + * 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. + * + * If one wishes to supply a callback after all the 'real' bounds update has happened, + * pass @param postBoundsUpdateCallback. + */ + private void startBoundsAnimator(float toX, float toY, Runnable postBoundsUpdateCallback) { + if (!mSpringingToTouch) { + cancelPhysicsAnimation(); + } + + setAnimatingToBounds(new Rect( + (int) toX, + (int) toY, + (int) toX + getBounds().width(), + (int) toY + getBounds().height())); + + if (!mTemporaryBoundsPhysicsAnimator.isRunning()) { + if (postBoundsUpdateCallback != null) { + mTemporaryBoundsPhysicsAnimator + .addUpdateListener(mResizePipUpdateListener) + .withEndActions(this::onBoundsPhysicsAnimationEnd, + postBoundsUpdateCallback); + } else { + mTemporaryBoundsPhysicsAnimator + .addUpdateListener(mResizePipUpdateListener) + .withEndActions(this::onBoundsPhysicsAnimationEnd); + } + } + + 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 onBoundsPhysicsAnimationEnd() { + // The physics animation ended, though we may not necessarily be done animating, such as + // when we're still dragging after moving out of the magnetic target. + if (!mDismissalPending + && !mSpringingToTouch + && !mMagnetizedPip.getObjectStuckToTarget()) { + // All motion operations have actually finished. + mPipBoundsState.setBounds( + mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); + mPipBoundsState.getMotionBoundsState().onAllAnimationsEnded(); + 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(getBounds()); + } + } + mPipBoundsState.getMotionBoundsState().onPhysicsAnimationEnded(); + mSpringingToTouch = false; + mDismissalPending = false; + } + + /** + * Notifies the floating coordinator that we're moving, and sets the animating to bounds so + * we return these bounds from + * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}. + */ + private void setAnimatingToBounds(Rect bounds) { + mPipBoundsState.getMotionBoundsState().setAnimatingToBounds(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(getBounds())) { + 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, + TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND, 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, mPipBoundsState.getMotionBoundsState().getBoundsInMotion(), + 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; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipPinchResizingAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipPinchResizingAlgorithm.java new file mode 100644 index 000000000000..805123f81d81 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipPinchResizingAlgorithm.java @@ -0,0 +1,126 @@ +/* + * 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.Point; +import android.graphics.Rect; + +/** + * Helper class to calculate the new size given two-fingers pinch to resize. + */ +public class PipPinchResizingAlgorithm { + private static final Rect TMP_RECT = new Rect(); + /** + * Given inputs and requirements and current PiP bounds, return the new size. + * + * @param x0 x-coordinate of the primary input. + * @param y0 y-coordinate of the primary input. + * @param x1 x-coordinate of the secondary input. + * @param y1 y-coordinate of the secondary input. + * @param downx0 x-coordinate of the original down point of the primary input. + * @param downy0 y-coordinate of the original down ponit of the primary input. + * @param downx1 x-coordinate of the original down point of the secondary input. + * @param downy1 y-coordinate of the original down point of the secondary input. + * @param currentPipBounds current PiP bounds. + * @param minVisibleWidth minimum visible width. + * @param minVisibleHeight minimum visible height. + * @param maxSize max size. + * @return The new resized PiP bounds, sharing the same center. + */ + public static Rect pinchResize(float x0, float y0, float x1, float y1, + float downx0, float downy0, float downx1, float downy1, Rect currentPipBounds, + int minVisibleWidth, int minVisibleHeight, Point maxSize) { + + int width = currentPipBounds.width(); + int height = currentPipBounds.height(); + int left = currentPipBounds.left; + int top = currentPipBounds.top; + int right = currentPipBounds.right; + int bottom = currentPipBounds.bottom; + final float aspect = (float) width / (float) height; + final int widthDelta = Math.round(Math.abs(x0 - x1) - Math.abs(downx0 - downx1)); + final int heightDelta = Math.round(Math.abs(y0 - y1) - Math.abs(downy0 - downy1)); + final int dx = (int) ((x0 - downx0 + x1 - downx1) / 2); + final int dy = (int) ((y0 - downy0 + y1 - downy1) / 2); + + width = Math.max(minVisibleWidth, Math.min(width + widthDelta, maxSize.x)); + height = Math.max(minVisibleHeight, Math.min(height + heightDelta, maxSize.y)); + + // Calculate 2 rectangles fulfilling all requirements for either X or Y being the major + // drag axis. What ever is producing the bigger rectangle will be chosen. + int width1; + int width2; + int height1; + int height2; + if (aspect > 1.0f) { + // Assuming that the width is our target we calculate the height. + width1 = Math.max(minVisibleWidth, Math.min(maxSize.x, width)); + height1 = Math.round((float) width1 / aspect); + if (height1 < minVisibleHeight) { + // If the resulting height is too small we adjust to the minimal size. + height1 = minVisibleHeight; + width1 = Math.max(minVisibleWidth, + Math.min(maxSize.x, Math.round((float) height1 * aspect))); + } + // Assuming that the height is our target we calculate the width. + height2 = Math.max(minVisibleHeight, Math.min(maxSize.y, height)); + width2 = Math.round((float) height2 * aspect); + if (width2 < minVisibleWidth) { + // If the resulting width is too small we adjust to the minimal size. + width2 = minVisibleWidth; + height2 = Math.max(minVisibleHeight, + Math.min(maxSize.y, Math.round((float) width2 / aspect))); + } + } else { + // Assuming that the width is our target we calculate the height. + width1 = Math.max(minVisibleWidth, Math.min(maxSize.x, width)); + height1 = Math.round((float) width1 / aspect); + if (height1 < minVisibleHeight) { + // If the resulting height is too small we adjust to the minimal size. + height1 = minVisibleHeight; + width1 = Math.max(minVisibleWidth, + Math.min(maxSize.x, Math.round((float) height1 * aspect))); + } + // Assuming that the height is our target we calculate the width. + height2 = Math.max(minVisibleHeight, Math.min(maxSize.y, height)); + width2 = Math.round((float) height2 * aspect); + if (width2 < minVisibleWidth) { + // If the resulting width is too small we adjust to the minimal size. + width2 = minVisibleWidth; + height2 = Math.max(minVisibleHeight, + Math.min(maxSize.y, Math.round((float) width2 / aspect))); + } + } + + // Use the bigger of the two rectangles if the major change was positive, otherwise + // do the opposite. + final boolean grows = width > (right - left) || height > (bottom - top); + if (grows == (width1 * height1 > width2 * height2)) { + width = width1; + height = height1; + } else { + width = width2; + height = height2; + } + + TMP_RECT.set(currentPipBounds.centerX() - width / 2, + currentPipBounds.centerY() - height / 2, + currentPipBounds.centerX() + width / 2, + currentPipBounds.centerY() + height / 2); + TMP_RECT.offset(dx, dy); + return TMP_RECT; + } +} 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..8fb358ad74d1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java @@ -0,0 +1,605 @@ +/* + * 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.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.ViewConfiguration; + +import androidx.annotation.VisibleForTesting; + +import com.android.internal.policy.TaskResizingAlgorithm; +import com.android.wm.shell.R; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.pip.PipAnimationController; +import com.android.wm.shell.pip.PipBoundsAlgorithm; +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; +import java.util.function.Consumer; +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 int PINCH_RESIZE_SNAP_DURATION = 250; + private static final int PINCH_RESIZE_MAX_ANGLE_ROTATION = 45; + + private final Context mContext; + private final PipBoundsAlgorithm mPipBoundsAlgorithm; + private final PipMotionHelper mMotionHelper; + private final PipBoundsState mPipBoundsState; + private final PipTaskOrganizer mPipTaskOrganizer; + private final PhonePipMenuController mPhonePipMenuController; + private final PipUiEventLogger mPipUiEventLogger; + private final int mDisplayId; + private final ShellExecutor mMainExecutor; + private final Region mTmpRegion = new Region(); + + private final PointF mDownPoint = new PointF(); + private final PointF mDownSecondaryPoint = 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; + // For drag-resize + private boolean mThresholdCrossed; + // For pinch-resize + private boolean mThresholdCrossed0; + private boolean mThresholdCrossed1; + private boolean mUsingPinchToZoom = false; + int mFirstIndex = -1; + int mSecondIndex = -1; + + private InputMonitor mInputMonitor; + private InputEventReceiver mInputEventReceiver; + + private int mCtrlType; + + public PipResizeGestureHandler(Context context, PipBoundsAlgorithm pipBoundsAlgorithm, + PipBoundsState pipBoundsState, PipMotionHelper motionHelper, + PipTaskOrganizer pipTaskOrganizer, Function<Rect, Rect> movementBoundsSupplier, + Runnable updateMovementBoundsRunnable, PipUiEventLogger pipUiEventLogger, + PhonePipMenuController menuActivityController, ShellExecutor mainExecutor) { + mContext = context; + mDisplayId = context.getDisplayId(); + mMainExecutor = mainExecutor; + mPipBoundsAlgorithm = pipBoundsAlgorithm; + mPipBoundsState = pipBoundsState; + mMotionHelper = motionHelper; + mPipTaskOrganizer = pipTaskOrganizer; + mMovementBoundsSupplier = movementBoundsSupplier; + mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable; + mPhonePipMenuController = menuActivityController; + mPipUiEventLogger = pipUiEventLogger; + + context.getDisplay().getRealSize(mMaxSize); + reloadResources(); + + 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); + try { + mMainExecutor.executeBlocking(() -> { + mInputEventReceiver = new PipResizeInputEventReceiver( + mInputMonitor.getInputChannel(), Looper.myLooper()); + }); + } catch (InterruptedException e) { + throw new RuntimeException("Failed to create input event receiver", e); + } + } + } + + private void onInputEvent(InputEvent ev) { + // Don't allow resize when PiP is stashed. + if (mPipBoundsState.isStashed()) { + return; + } + + if (ev instanceof MotionEvent) { + if (mUsingPinchToZoom) { + onPinchResize((MotionEvent) ev); + } else { + onDragCornerResize((MotionEvent) ev); + } + } + } + + /** + * Checks if there is currently an on-going gesture, either drag-resize or pinch-resize. + */ + public boolean hasOngoingGesture() { + return mCtrlType != CTRL_NONE || mUsingPinchToZoom; + } + + /** + * 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 isWithinDragResizeRegion(int x, int y) { + final Rect currentPipBounds = mPipBoundsState.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: + if (isWithinDragResizeRegion((int) ev.getRawX(), (int) ev.getRawY())) { + return true; + } + break; + + case MotionEvent.ACTION_POINTER_DOWN: + if (mEnablePinchResize && ev.getPointerCount() == 2) { + onPinchResize(ev); + mUsingPinchToZoom = true; + return true; + } + break; + + default: + break; + } + } + return false; + } + + private void setCtrlType(int x, int y) { + final Rect currentPipBounds = mPipBoundsState.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 onPinchResize(MotionEvent ev) { + int action = ev.getActionMasked(); + + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + mFirstIndex = -1; + mSecondIndex = -1; + finishResize(); + } + + if (ev.getPointerCount() != 2) { + return; + } + + if (action == MotionEvent.ACTION_POINTER_DOWN) { + if (mFirstIndex == -1 && mSecondIndex == -1) { + mFirstIndex = 0; + mSecondIndex = 1; + mDownPoint.set(ev.getRawX(mFirstIndex), ev.getRawY(mFirstIndex)); + mDownSecondaryPoint.set(ev.getRawX(mSecondIndex), ev.getRawY(mSecondIndex)); + + + mLastDownBounds.set(mPipBoundsState.getBounds()); + mLastResizeBounds.set(mLastDownBounds); + } + } + + if (action == MotionEvent.ACTION_MOVE) { + if (mFirstIndex == -1 || mSecondIndex == -1) { + return; + } + + float x0 = ev.getRawX(mFirstIndex); + float y0 = ev.getRawY(mFirstIndex); + float x1 = ev.getRawX(mSecondIndex); + float y1 = ev.getRawY(mSecondIndex); + + double hypot0 = Math.hypot(x0 - mDownPoint.x, y0 - mDownPoint.y); + double hypot1 = Math.hypot(x1 - mDownSecondaryPoint.x, y1 - mDownSecondaryPoint.y); + // Capture inputs + if (hypot0 > mTouchSlop && !mThresholdCrossed0) { + mInputMonitor.pilferPointers(); + mThresholdCrossed0 = true; + // Reset the down to begin resizing from this point + mDownPoint.set(x0, y0); + } + if (hypot1 > mTouchSlop && !mThresholdCrossed1) { + mInputMonitor.pilferPointers(); + mThresholdCrossed1 = true; + // Reset the down to begin resizing from this point + mDownSecondaryPoint.set(x1, y1); + } + if (mThresholdCrossed0 || mThresholdCrossed1) { + if (mPhonePipMenuController.isMenuVisible()) { + mPhonePipMenuController.hideMenu(); + } + + x0 = mThresholdCrossed0 ? x0 : mDownPoint.x; + y0 = mThresholdCrossed0 ? y0 : mDownPoint.y; + x1 = mThresholdCrossed1 ? x1 : mDownSecondaryPoint.x; + y1 = mThresholdCrossed1 ? y1 : mDownSecondaryPoint.y; + + final Rect originalPipBounds = mPipBoundsState.getBounds(); + int focusX = (int) originalPipBounds.centerX(); + int focusY = (int) originalPipBounds.centerY(); + + float down0X = mDownPoint.x; + float down0Y = mDownPoint.y; + float down1X = mDownSecondaryPoint.x; + float down1Y = mDownSecondaryPoint.y; + + // Top right + Bottom left pinch to zoom. + if ((down0X > focusX && down0Y < focusY && down1X < focusX && down1Y > focusY) + || (down1X > focusX && down1Y < focusY + && down0X < focusX && down0Y > focusY)) { + mAngle = calculateRotationAngle(mLastResizeBounds.centerX(), + mLastResizeBounds.centerY(), x0, y0, x1, y1, true); + } else if ((down0X < focusX && down0Y < focusY + && down1X > focusX && down1Y > focusY) + || (down1X < focusX && down1Y < focusY + && down0X > focusX && down0Y > focusY)) { + mAngle = calculateRotationAngle(mLastResizeBounds.centerX(), + mLastResizeBounds.centerY(), x0, y0, x1, y1, false); + } + + mLastResizeBounds.set(PipPinchResizingAlgorithm.pinchResize(x0, y0, x1, y1, + mDownPoint.x, mDownPoint.y, mDownSecondaryPoint.x, mDownSecondaryPoint.y, + originalPipBounds, mMinSize.x, mMinSize.y, mMaxSize)); + + mPipTaskOrganizer.scheduleUserResizePip(mLastDownBounds, mLastResizeBounds, + (float) -mAngle, null); + mPipBoundsState.setHasUserResizedPip(true); + } + } + } + + private float mAngle = 0; + + private float calculateRotationAngle(int focusX, int focusY, float x0, float y0, + float x1, float y1, boolean positive) { + + // The base angle is the angle formed by taking the angle between the center horizontal + // and one of the corners. + double baseAngle = Math.toDegrees(Math.atan2(Math.abs(mLastResizeBounds.top - focusY), + Math.abs(mLastResizeBounds.right - focusX))); + double angle0 = mThresholdCrossed0 + ? Math.toDegrees(Math.atan2(Math.abs(y0 - focusY), Math.abs(x0 - focusX))) + : baseAngle; + double angle1 = mThresholdCrossed1 + ? Math.toDegrees(Math.atan2(Math.abs(y1 - focusY), Math.abs(x1 - focusX))) + : baseAngle; + + // Calculate the percentage difference of [0, 90] compare to the base angle. + double diff0 = (Math.max(0, Math.min(angle0, 90)) - baseAngle) / 90; + double diff1 = (Math.max(0, Math.min(angle1, 90)) - baseAngle) / 90; + + return (float) (diff0 + diff1) / 2 * PINCH_RESIZE_MAX_ANGLE_ROTATION * (positive ? 1 : -1); + } + + 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 = mPipBoundsState.getBounds(); + mLastResizeBounds.setEmpty(); + mAllowGesture = isInValidSysUiState() && isWithinDragResizeRegion((int) x, (int) y); + if (mAllowGesture) { + setCtrlType((int) x, (int) y); + mDownPoint.set(x, y); + mLastDownBounds.set(mPipBoundsState.getBounds()); + } + if (!currentPipBounds.contains((int) ev.getX(), (int) ev.getY()) + && mPhonePipMenuController.isMenuVisible()) { + mPhonePipMenuController.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 (mPhonePipMenuController.isMenuVisible()) { + mPhonePipMenuController.hideMenuWithoutResize(); + mPhonePipMenuController.hideMenu(); + } + final Rect currentPipBounds = mPipBoundsState.getBounds(); + mLastResizeBounds.set(TaskResizingAlgorithm.resizeDrag(x, y, + mDownPoint.x, mDownPoint.y, currentPipBounds, mCtrlType, mMinSize.x, + mMinSize.y, mMaxSize, true, + mLastDownBounds.width() > mLastDownBounds.height())); + mPipBoundsAlgorithm.transformBoundsToAspectRatio(mLastResizeBounds, + mPipBoundsState.getAspectRatio(), false /* useCurrentMinEdgeSize */, + true /* useCurrentSize */); + mPipTaskOrganizer.scheduleUserResizePip(mLastDownBounds, mLastResizeBounds, + null); + mPipBoundsState.setHasUserResizedPip(true); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + finishResize(); + break; + } + } + } + + private void finishResize() { + if (!mLastResizeBounds.isEmpty()) { + final Consumer<Rect> callback = (rect) -> { + mUserResizeBounds.set(mLastResizeBounds); + mMotionHelper.synchronizePinnedStackBounds(); + mUpdateMovementBoundsRunnable.run(); + resetState(); + }; + + // Pinch-to-resize needs to re-calculate snap fraction and animate to the snapped + // position correctly. Drag-resize does not need to move, so just finalize resize. + if (mUsingPinchToZoom) { + final Rect startBounds = new Rect(mLastResizeBounds); + mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, + mPipBoundsAlgorithm.getSnapFraction(mPipBoundsState.getBounds())); + mPipTaskOrganizer.scheduleAnimateResizePip(startBounds, mLastResizeBounds, + PINCH_RESIZE_SNAP_DURATION, -mAngle, callback); + } else { + mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds, + PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE, callback); + } + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_RESIZE); + } else { + resetState(); + } + } + + private void resetState() { + mCtrlType = CTRL_NONE; + mAngle = 0; + 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 PipResizeInputEventReceiver extends BatchedInputEventReceiver { + PipResizeInputEventReceiver(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..3cb3ae89b5f5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java @@ -0,0 +1,1025 @@ +/* + * 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.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_STASH_MINIMUM_VELOCITY_THRESHOLD; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; +import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE; +import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_CLOSE; +import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL; +import static com.android.wm.shell.pip.phone.PhonePipMenuController.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.provider.DeviceConfig; +import android.util.Log; +import android.util.Size; +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.common.ShellExecutor; +import com.android.wm.shell.pip.PipAnimationController; +import com.android.wm.shell.pip.PipBoundsAlgorithm; +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; +import java.lang.ref.WeakReference; +import java.util.function.Consumer; + +/** + * 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"; + private static final float MINIMUM_SIZE_PERCENT = 0.4f; + private static final float DEFAULT_STASH_VELOCITY_THRESHOLD = 18000.f; + + // Allow PIP to resize to a slightly bigger state upon touch + private final boolean mEnableResize; + private final Context mContext; + private final PipBoundsAlgorithm mPipBoundsAlgorithm; + private final @NonNull PipBoundsState mPipBoundsState; + private final PipUiEventLogger mPipUiEventLogger; + private final PipDismissTargetHandler mPipDismissTargetHandler; + + private PipResizeGestureHandler mPipResizeGestureHandler; + private WeakReference<Consumer<Rect>> mPipExclusionBoundsChangeListener; + + private final PhonePipMenuController mMenuController; + private final AccessibilityManager mAccessibilityManager; + private boolean mShowPipMenuOnAnimationEnd = false; + + /** + * Whether PIP stash is enabled or not. When enabled, if the user flings toward the edge of the + * screen, it will be shown in "stashed" mode, where PIP will only show partially. + */ + private boolean mEnableStash = true; + + private float mStashVelocityThreshold; + + // The reference inset bounds, used to determine the dismiss fraction + private final Rect mInsetBounds = 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 final PipAccessibilityInteractionConnection mConnection; + + // 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; + + // 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 PhonePipMenuController.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, mPipBoundsState.getBounds(), + true /* allowMenuTimeout */, willResizeMenu(), shouldShowResizeHandle()); + } + } + + @SuppressLint("InflateParams") + public PipTouchHandler(Context context, + PhonePipMenuController menuController, + PipBoundsAlgorithm pipBoundsAlgorithm, + @NonNull PipBoundsState pipBoundsState, + PipTaskOrganizer pipTaskOrganizer, + FloatingContentCoordinator floatingContentCoordinator, + PipUiEventLogger pipUiEventLogger, + ShellExecutor mainExecutor) { + // Initialize the Pip input consumer + mContext = context; + mAccessibilityManager = context.getSystemService(AccessibilityManager.class); + mPipBoundsAlgorithm = pipBoundsAlgorithm; + mPipBoundsState = pipBoundsState; + mMenuController = menuController; + mMenuController.addListener(new PipMenuListener()); + mGesture = new DefaultPipTouchGesture(); + mMotionHelper = new PipMotionHelper(mContext, pipBoundsState, pipTaskOrganizer, + mMenuController, mPipBoundsAlgorithm.getSnapAlgorithm(), + floatingContentCoordinator, mainExecutor); + mPipResizeGestureHandler = + new PipResizeGestureHandler(context, pipBoundsAlgorithm, pipBoundsState, + mMotionHelper, pipTaskOrganizer, this::getMovementBounds, + this::updateMovementBounds, pipUiEventLogger, menuController, + mainExecutor); + mPipDismissTargetHandler = new PipDismissTargetHandler(context, pipUiEventLogger, + mMotionHelper, mainExecutor); + mTouchState = new PipTouchState(ViewConfiguration.get(context), + () -> { + if (mPipBoundsState.isStashed()) { + animateToUnStashedState(); + mPipBoundsState.setStashed(STASH_TYPE_NONE); + } else { + mMenuController.showMenuWithPossibleDelay(MENU_STATE_FULL, + mPipBoundsState.getBounds(), true /* allowMenuTimeout */, + willResizeMenu(), + shouldShowResizeHandle()); + } + }, + menuController::hideMenu, + mainExecutor); + + Resources res = context.getResources(); + mEnableResize = res.getBoolean(R.bool.config_pipEnableResizeForMenu); + reloadResources(); + + mFloatingContentCoordinator = floatingContentCoordinator; + mConnection = new PipAccessibilityInteractionConnection(mContext, pipBoundsState, + mMotionHelper, pipTaskOrganizer, mPipBoundsAlgorithm.getSnapAlgorithm(), + this::onAccessibilityShowMenu, this::updateMovementBounds, mainExecutor); + + mPipUiEventLogger = pipUiEventLogger; + + mEnableStash = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_SYSTEMUI, + PIP_STASHING, + /* defaultValue = */ true); + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, + mainExecutor, + properties -> { + if (properties.getKeyset().contains(PIP_STASHING)) { + mEnableStash = properties.getBoolean( + PIP_STASHING, /* defaultValue = */ true); + } + }); + mStashVelocityThreshold = DeviceConfig.getFloat( + DeviceConfig.NAMESPACE_SYSTEMUI, + PIP_STASH_MINIMUM_VELOCITY_THRESHOLD, + DEFAULT_STASH_VELOCITY_THRESHOLD); + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, + mainExecutor, + properties -> { + if (properties.getKeyset().contains(PIP_STASH_MINIMUM_VELOCITY_THRESHOLD)) { + mStashVelocityThreshold = properties.getFloat( + PIP_STASH_MINIMUM_VELOCITY_THRESHOLD, + DEFAULT_STASH_VELOCITY_THRESHOLD); + } + }); + } + + 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, mPipBoundsState.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); + } + // Reset exclusion to none. + if (mPipExclusionBoundsChangeListener != null + && mPipExclusionBoundsChangeListener.get() != null) { + mPipExclusionBoundsChangeListener.get().accept(new Rect()); + } + 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(mPipBoundsState.getBounds()); + } + + if (mShowPipMenuOnAnimationEnd) { + mMenuController.showMenu(MENU_STATE_CLOSE, mPipBoundsState.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(); + mPipBoundsAlgorithm.getMovementBounds(outBounds, insetBounds, toMovementBounds, 0); + final int prevBottom = mPipBoundsState.getMovementBounds().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 + Rect normalMovementBounds = new Rect(); + mPipBoundsAlgorithm.getMovementBounds(normalBounds, insetBounds, + normalMovementBounds, bottomOffset); + + if (mPipBoundsState.getMovementBounds().isEmpty()) { + // mMovementBounds is not initialized yet and a clean movement bounds without + // bottom offset shall be used later in this function. + mPipBoundsAlgorithm.getMovementBounds(curBounds, insetBounds, + mPipBoundsState.getMovementBounds(), 0 /* bottomOffset */); + } + + // Calculate the expanded size + float aspectRatio = (float) normalBounds.width() / normalBounds.height(); + Point displaySize = new Point(); + mContext.getDisplay().getRealSize(displaySize); + Size expandedSize = mPipBoundsAlgorithm.getSizeForAspectRatio( + aspectRatio, mExpandedShortestEdgeSize, displaySize.x, displaySize.y); + mPipBoundsState.setExpandedBounds( + new Rect(0, 0, expandedSize.getWidth(), expandedSize.getHeight())); + Rect expandedMovementBounds = new Rect(); + mPipBoundsAlgorithm.getMovementBounds( + mPipBoundsState.getExpandedBounds(), insetBounds, expandedMovementBounds, + bottomOffset); + + if (mPipResizeGestureHandler.isUsingPinchToZoom()) { + updatePinchResizeSizeConstraints(insetBounds, normalBounds, aspectRatio); + } else { + mPipResizeGestureHandler.updateMinSize(normalBounds.width(), normalBounds.height()); + mPipResizeGestureHandler.updateMaxSize(mPipBoundsState.getExpandedBounds().width(), + mPipBoundsState.getExpandedBounds().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(); + mPipBoundsAlgorithm.getMovementBounds(curBounds, insetBounds, + toMovementBounds, mIsImeShowing ? mImeHeight : 0); + final int prevBottom = mPipBoundsState.getMovementBounds().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(mPipBoundsState.getExpandedBounds()); + mPipBoundsAlgorithm.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 + mPipBoundsState.setNormalMovementBounds(normalMovementBounds); + mPipBoundsState.setExpandedMovementBounds(expandedMovementBounds); + mDisplayRotation = displayRotation; + mInsetBounds.set(insetBounds); + updateMovementBounds(); + mMovementBoundsExtraOffsets = extraOffset; + mConnection.onMovementBoundsChanged(normalBounds, mPipBoundsState.getExpandedBounds(), + mPipBoundsState.getNormalMovementBounds(), + mPipBoundsState.getExpandedMovementBounds()); + + // If we have a deferred resize, apply it now + if (mDeferResizeToNormalBoundsUntilRotation == displayRotation) { + mMotionHelper.animateToUnexpandedState(normalBounds, mSavedSnapFraction, + mPipBoundsState.getNormalMovementBounds(), mPipBoundsState.getMovementBounds(), + true /* immediate */); + mSavedSnapFraction = -1f; + mDeferResizeToNormalBoundsUntilRotation = -1; + } + } + + private void updatePinchResizeSizeConstraints(Rect insetBounds, Rect normalBounds, + float aspectRatio) { + final int shorterLength = Math.min(mPipBoundsState.getDisplayBounds().width(), + mPipBoundsState.getDisplayBounds().height()); + final int totalPadding = insetBounds.left * 2; + final int minWidth, minHeight, maxWidth, maxHeight; + if (aspectRatio > 1f) { + minWidth = (int) Math.min(normalBounds.width(), shorterLength * MINIMUM_SIZE_PERCENT); + minHeight = (int) (minWidth / aspectRatio); + maxWidth = (int) Math.max(normalBounds.width(), shorterLength - totalPadding); + maxHeight = (int) (maxWidth / aspectRatio); + } else { + minHeight = (int) Math.min(normalBounds.height(), shorterLength * MINIMUM_SIZE_PERCENT); + minWidth = (int) (minHeight * aspectRatio); + maxHeight = (int) Math.max(normalBounds.height(), shorterLength - totalPadding); + maxWidth = (int) (maxHeight * aspectRatio); + } + + mPipResizeGestureHandler.updateMinSize(minWidth, minHeight); + mPipResizeGestureHandler.updateMaxSize(maxWidth, maxHeight); + mPipBoundsState.setMaxSize(maxWidth, maxHeight); + mPipBoundsState.setMinSize(minWidth, minHeight); + } + + /** + * TODO Add appropriate description + */ + public void onRegistrationChanged(boolean isRegistered) { + if (isRegistered) { + mConnection.register(mAccessibilityManager); + } else { + mAccessibilityManager.setPictureInPictureActionReplacingConnection(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, mPipBoundsState.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; + } + + MotionEvent ev = (MotionEvent) inputEvent; + if (!mPipBoundsState.isStashed() && 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 (mPipResizeGestureHandler.hasOngoingGesture()) { + mPipDismissTargetHandler.hideDismissTargetMaybe(); + 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, mPipBoundsState.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; + } + } + + shouldDeliverToMenu |= !mPipBoundsState.isStashed(); + + // 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 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 + int displayRotation = mContext.getDisplay().getRotation(); + if (mDisplayRotation != displayRotation) { + mDeferResizeToNormalBoundsUntilRotation = displayRotation; + } + } + + 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 animateToMaximizedState(Runnable callback) { + Rect maxMovementBounds = new Rect(); + Rect maxBounds = new Rect(0, 0, mPipBoundsState.getMaxSize().x, + mPipBoundsState.getMaxSize().y); + mPipBoundsAlgorithm.getMovementBounds(maxBounds, mInsetBounds, maxMovementBounds, + mIsImeShowing ? mImeHeight : 0); + mSavedSnapFraction = mMotionHelper.animateToExpandedState(maxBounds, + mPipBoundsState.getMovementBounds(), maxMovementBounds, + callback); + } + + private void animateToMinimizedState() { + animateToUnexpandedState(new Rect(0, 0, mPipBoundsState.getMinSize().x, + mPipBoundsState.getMinSize().y)); + } + + private void animateToExpandedState(Runnable callback) { + mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); + final Rect currentBounds = mPipBoundsState.getBounds(); + final Rect expandedBounds = mPipBoundsState.getExpandedBounds(); + Rect finalExpandedBounds = new Rect(expandedBounds.width() > expandedBounds.width() + && expandedBounds.height() > expandedBounds.height() + ? currentBounds : expandedBounds); + Rect restoredMovementBounds = new Rect(); + mPipBoundsAlgorithm.getMovementBounds(finalExpandedBounds, + mInsetBounds, restoredMovementBounds, mIsImeShowing ? mImeHeight : 0); + mSavedSnapFraction = mMotionHelper.animateToExpandedState(finalExpandedBounds, + mPipBoundsState.getMovementBounds(), restoredMovementBounds, callback); + } + + private void animateToUnexpandedState(Rect restoreBounds) { + Rect restoredMovementBounds = new Rect(); + mPipBoundsAlgorithm.getMovementBounds(restoreBounds, + mInsetBounds, restoredMovementBounds, mIsImeShowing ? mImeHeight : 0); + mMotionHelper.animateToUnexpandedState(restoreBounds, mSavedSnapFraction, + restoredMovementBounds, mPipBoundsState.getMovementBounds(), false /* immediate */); + mSavedSnapFraction = -1f; + } + + private void animateToUnStashedState() { + final Rect pipBounds = mPipBoundsState.getBounds(); + final boolean onLeftEdge = pipBounds.left < mPipBoundsState.getDisplayBounds().left; + final Rect unStashedBounds = new Rect(0, pipBounds.top, 0, pipBounds.bottom); + unStashedBounds.left = onLeftEdge ? mInsetBounds.left + : mInsetBounds.right - pipBounds.width(); + unStashedBounds.right = onLeftEdge ? mInsetBounds.left + pipBounds.width() + : mInsetBounds.right; + mMotionHelper.animateToUnStashedBounds(unStashedBounds); + } + + /** + * @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; + } + + 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; + private float mDownSavedFraction = -1f; + + @Override + public void onDown(PipTouchState touchState) { + if (!touchState.isUserInteracting()) { + return; + } + + Rect bounds = getPossiblyMotionBounds(); + mDelta.set(0f, 0f); + mStartPosition.set(bounds.left, bounds.top); + mMovementWithinDismiss = touchState.getDownTouchPosition().y + >= mPipBoundsState.getMovementBounds().bottom; + mMotionHelper.setSpringingToTouch(false); + mDownSavedFraction = mPipBoundsAlgorithm.getSnapFraction(mPipBoundsState.getBounds()); + + // 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 && !mPipBoundsState.isStashed()) { + mMenuController.pokeMenu(); + } + } + + @Override + public boolean onMove(PipTouchState touchState) { + if (!touchState.isUserInteracting()) { + return false; + } + + if (touchState.startedDragging()) { + mPipBoundsState.setStashed(STASH_TYPE_NONE); + 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(getPossiblyMotionBounds()); + 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 >= mPipBoundsState.getMovementBounds().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, mPipBoundsState.getBounds(), + true /* allowMenuTimeout */, willResizeMenu(), + shouldShowResizeHandle()); + } + mShouldHideMenuAfterFling = mMenuState == MENU_STATE_NONE; + + // Reset the touch state on up before the fling settles + mTouchState.reset(); + if (mEnableStash && !mPipBoundsState.isStashed() + && shouldStash(vel, getPossiblyMotionBounds())) { + mMotionHelper.stashToEdge(vel.x, this::stashEndAction /* endAction */); + } else { + mMotionHelper.flingToSnapTarget(vel.x, vel.y, + this::flingEndAction /* endAction */); + } + } else if (mTouchState.isDoubleTap() && !mPipBoundsState.isStashed()) { + // If using pinch to zoom, double-tap functions as resizing between max/min size + if (mPipResizeGestureHandler.isUsingPinchToZoom()) { + final boolean toExpand = mPipBoundsState.getBounds().width() + < mPipBoundsState.getMaxSize().x + && mPipBoundsState.getBounds().height() + < mPipBoundsState.getMaxSize().y; + if (toExpand) { + mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); + animateToMaximizedState(null); + } else { + animateToUnexpandedState(getUserResizeBounds()); + } + } 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()) { + if (mPipBoundsState.isStashed()) { + animateToUnStashedState(); + mPipBoundsState.setStashed(STASH_TYPE_NONE); + } else { + // 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, mPipBoundsState.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(); + } + } + mDownSavedFraction = -1f; + return true; + } + + private void stashEndAction() { + if (mPipExclusionBoundsChangeListener != null + && mPipExclusionBoundsChangeListener.get() != null) { + mPipExclusionBoundsChangeListener.get().accept(mPipBoundsState.getBounds()); + } + } + + 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(); + } + // Reset exclusion to none. + if (mPipExclusionBoundsChangeListener != null + && mPipExclusionBoundsChangeListener.get() != null) { + mPipExclusionBoundsChangeListener.get().accept(new Rect()); + } + } + + private boolean shouldStash(PointF vel, Rect motionBounds) { + // If user flings the PIP window above the minimum velocity, stash PIP. + // Only allow stashing to the edge if the user starts dragging the PIP from the + // opposite edge. + final boolean stashFromFlingToEdge = ((vel.x < -mStashVelocityThreshold + && mDownSavedFraction > 1f && mDownSavedFraction < 2f) + || (vel.x > mStashVelocityThreshold + && mDownSavedFraction > 3f && mDownSavedFraction < 4f)); + + // If User releases the PIP window while it's out of the display bounds, put + // PIP into stashed mode. + final int offset = motionBounds.width() / 2; + final boolean stashFromDroppingOnEdge = + (motionBounds.right > mPipBoundsState.getDisplayBounds().right + offset + || motionBounds.left + < mPipBoundsState.getDisplayBounds().left - offset); + + return stashFromFlingToEdge || stashFromDroppingOnEdge; + } + } + + void setPipExclusionBoundsChangeListener(Consumer<Rect> pipExclusionBoundsChangeListener) { + mPipExclusionBoundsChangeListener = new WeakReference<>(pipExclusionBoundsChangeListener); + pipExclusionBoundsChangeListener.accept(mPipBoundsState.getBounds()); + } + + /** + * Updates the current movement bounds based on whether the menu is currently visible and + * resized. + */ + private void updateMovementBounds() { + mPipBoundsAlgorithm.getMovementBounds(mPipBoundsState.getBounds(), + mInsetBounds, mPipBoundsState.getMovementBounds(), mIsImeShowing ? mImeHeight : 0); + mMotionHelper.onMovementBoundsChanged(); + + boolean isMenuExpanded = mMenuState == MENU_STATE_FULL; + mPipBoundsState.setMinEdgeSize( + isMenuExpanded && willResizeMenu() ? mExpandedShortestEdgeSize + : mPipBoundsAlgorithm.getDefaultMinSize()); + } + + private Rect getMovementBounds(Rect curBounds) { + Rect movementBounds = new Rect(); + mPipBoundsAlgorithm.getMovementBounds(curBounds, mInsetBounds, + movementBounds, mIsImeShowing ? mImeHeight : 0); + return movementBounds; + } + + /** + * @return {@code true} if the menu should be resized on tap because app explicitly specifies + * PiP window size that is too small to hold all the actions. + */ + private boolean willResizeMenu() { + if (!mEnableResize) { + return false; + } + final Size estimatedMinMenuSize = mMenuController.getEstimatedMinMenuSize(); + if (estimatedMinMenuSize == null) { + Log.wtf(TAG, "Failed to get estimated menu size"); + return false; + } + final Rect currentBounds = mPipBoundsState.getBounds(); + return currentBounds.width() < estimatedMinMenuSize.getWidth() + || currentBounds.height() < estimatedMinMenuSize.getHeight(); + } + + /** + * Returns the PIP bounds if we're not in the middle of a motion operation, or the current, + * temporary motion bounds otherwise. + */ + Rect getPossiblyMotionBounds() { + return mPipBoundsState.getMotionBoundsState().isInMotion() + ? mPipBoundsState.getMotionBoundsState().getBoundsInMotion() + : mPipBoundsState.getBounds(); + } + + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + 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); + mPipBoundsAlgorithm.dump(pw, innerPrefix); + mTouchState.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..53303ff2b679 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java @@ -0,0 +1,404 @@ +/* + * 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.Display; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.common.ShellExecutor; + +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 ShellExecutor mMainExecutor; + 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; + private int mLastTouchDisplayId = Display.INVALID_DISPLAY; + + public PipTouchState(ViewConfiguration viewConfig, Runnable doubleTapTimeoutCallback, + Runnable hoverExitTimeoutCallback, ShellExecutor mainExecutor) { + mViewConfig = viewConfig; + mDoubleTapTimeoutCallback = doubleTapTimeoutCallback; + mHoverExitTimeoutCallback = hoverExitTimeoutCallback; + mMainExecutor = mainExecutor; + } + + /** + * Resets this state. + */ + public void reset() { + mAllowDraggingOffscreen = false; + mIsDragging = false; + mStartedDragging = false; + mIsUserInteracting = false; + mLastTouchDisplayId = Display.INVALID_DISPLAY; + } + + /** + * Processes a given touch event and updates the state. + */ + public void onTouchEvent(MotionEvent ev) { + mLastTouchDisplayId = ev.getDisplayId(); + 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) { + mMainExecutor.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; + } + + /** + * @return Display ID of the last touch event. + */ + public int getLastTouchDisplayId() { + return mLastTouchDisplayId; + } + + /** + * 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(); + mMainExecutor.removeCallbacks(mDoubleTapTimeoutCallback); + mMainExecutor.executeDelayed(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; + mMainExecutor.removeCallbacks(mDoubleTapTimeoutCallback); + } + + @VisibleForTesting + public void scheduleHoverExitTimeoutCallback() { + mMainExecutor.removeCallbacks(mHoverExitTimeoutCallback); + mMainExecutor.executeDelayed(mHoverExitTimeoutCallback, HOVER_EXIT_TIMEOUT); + } + + void removeHoverExitTimeoutCallback() { + mMainExecutor.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 + "mLastTouchDisplayId=" + mLastTouchDisplayId); + 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/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java new file mode 100644 index 000000000000..75fc9f5a4ecf --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java @@ -0,0 +1,464 @@ +/* + * 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.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; + +import android.annotation.IntDef; +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.RemoteAction; +import android.app.TaskInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ParceledListSlice; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Rect; +import android.os.RemoteException; +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.common.DisplayLayout; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TaskStackListenerCallback; +import com.android.wm.shell.common.TaskStackListenerImpl; +import com.android.wm.shell.pip.PinnedStackListenerForwarder; +import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.pip.PipBoundsAlgorithm; +import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipMediaController; +import com.android.wm.shell.pip.PipTaskOrganizer; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Manages the picture-in-picture (PIP) UI and states. + */ +public class TvPipController implements PipTaskOrganizer.PipTransitionCallback, + TvPipMenuController.Delegate, TvPipNotificationController.Delegate { + private static final String TAG = "TvPipController"; + static final boolean DEBUG = true; + + private static final int NONEXISTENT_TASK_ID = -1; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "STATE_" }, value = { + STATE_NO_PIP, + STATE_PIP, + STATE_PIP_MENU + }) + public @interface State {} + + /** + * State when there is no applications in Pip. + */ + private static final int STATE_NO_PIP = 0; + /** + * State when there is an applications in Pip and the Pip window located at its "normal" place + * (usually the bottom right corner). + */ + private static final int STATE_PIP = 1; + /** + * State when there is an applications in Pip and the Pip menu is open. In this state Pip window + * is usually moved from its "normal" position on the screen to the "menu" position - which is + * often at the middle of the screen, and gets slightly scaled up. + */ + private static final int STATE_PIP_MENU = 2; + + private final Context mContext; + + private final PipBoundsState mPipBoundsState; + private final PipBoundsAlgorithm mPipBoundsAlgorithm; + private final PipTaskOrganizer mPipTaskOrganizer; + private final PipMediaController mPipMediaController; + private final TvPipNotificationController mPipNotificationController; + private final TvPipMenuController mTvPipMenuController; + private final ShellExecutor mMainExecutor; + private final TvPipImpl mImpl = new TvPipImpl(); + + private @State int mState = STATE_NO_PIP; + private int mPinnedTaskId = NONEXISTENT_TASK_ID; + + private int mResizeAnimationDuration; + + public static Pip create( + Context context, + PipBoundsState pipBoundsState, + PipBoundsAlgorithm pipBoundsAlgorithm, + PipTaskOrganizer pipTaskOrganizer, + TvPipMenuController tvPipMenuController, + PipMediaController pipMediaController, + TvPipNotificationController pipNotificationController, + TaskStackListenerImpl taskStackListener, + WindowManagerShellWrapper wmShell, + ShellExecutor mainExecutor) { + return new TvPipController( + context, + pipBoundsState, + pipBoundsAlgorithm, + pipTaskOrganizer, + tvPipMenuController, + pipMediaController, + pipNotificationController, + taskStackListener, + wmShell, + mainExecutor).mImpl; + } + + private TvPipController( + Context context, + PipBoundsState pipBoundsState, + PipBoundsAlgorithm pipBoundsAlgorithm, + PipTaskOrganizer pipTaskOrganizer, + TvPipMenuController tvPipMenuController, + PipMediaController pipMediaController, + TvPipNotificationController pipNotificationController, + TaskStackListenerImpl taskStackListener, + WindowManagerShellWrapper wmShell, + ShellExecutor mainExecutor) { + mContext = context; + mMainExecutor = mainExecutor; + + mPipBoundsState = pipBoundsState; + mPipBoundsState.setDisplayId(context.getDisplayId()); + mPipBoundsState.setDisplayLayout(new DisplayLayout(context, context.getDisplay())); + mPipBoundsAlgorithm = pipBoundsAlgorithm; + + mPipMediaController = pipMediaController; + + mPipNotificationController = pipNotificationController; + mPipNotificationController.setDelegate(this); + + mTvPipMenuController = tvPipMenuController; + mTvPipMenuController.setDelegate(this); + + mPipTaskOrganizer = pipTaskOrganizer; + mPipTaskOrganizer.registerPipTransitionCallback(this); + + loadConfigurations(); + + registerTaskStackListenerCallback(taskStackListener); + registerWmShellPinnedStackListener(wmShell); + } + + private void onConfigurationChanged(Configuration newConfig) { + if (DEBUG) Log.d(TAG, "onConfigurationChanged(), state=" + stateToName(mState)); + + if (isPipShown()) { + if (DEBUG) Log.d(TAG, " > closing Pip."); + closePip(); + } + + loadConfigurations(); + mPipNotificationController.onConfigurationChanged(mContext); + } + + /** + * Returns {@code true} if Pip is shown. + */ + private boolean isPipShown() { + return mState != STATE_NO_PIP; + } + + /** + * Starts the process if bringing up the Pip menu if by issuing a command to move Pip + * task/window to the "Menu" position. We'll show the actual Menu UI (eg. actions) once the Pip + * task/window is properly positioned in {@link #onPipTransitionFinished(ComponentName, int)}. + */ + @Override + public void showPictureInPictureMenu() { + if (DEBUG) Log.d(TAG, "showPictureInPictureMenu(), state=" + stateToName(mState)); + + if (mState != STATE_PIP) { + if (DEBUG) Log.d(TAG, " > cannot open Menu from the current state."); + return; + } + + setState(STATE_PIP_MENU); + resizePinnedStack(STATE_PIP_MENU); + } + + /** + * Moves Pip window to its "normal" position. + */ + @Override + public void movePipToNormalPosition() { + if (DEBUG) Log.d(TAG, "movePipToNormalPosition(), state=" + stateToName(mState)); + + setState(STATE_PIP); + resizePinnedStack(STATE_PIP); + } + + /** + * Opens the "Pip-ed" Activity fullscreen. + */ + @Override + public void movePipToFullscreen() { + if (DEBUG) Log.d(TAG, "movePipToFullscreen(), state=" + stateToName(mState)); + + mPipTaskOrganizer.exitPip(mResizeAnimationDuration); + onPipDisappeared(); + } + + /** + * Closes Pip window. + */ + @Override + public void closePip() { + if (DEBUG) Log.d(TAG, "closePip(), state=" + stateToName(mState)); + + removeTask(mPinnedTaskId); + onPipDisappeared(); + } + + /** + * Resizes the Pip task/window to the appropriate size for the given state. + * This is a legacy API. Now we expect that the state argument passed to it should always match + * the current state of the Controller. If it does not match an {@link IllegalArgumentException} + * will be thrown. However, if the passed state does match - we'll determine the right bounds + * to the state and will move Pip task/window there. + * + * @param state the to determine the Pip bounds. IMPORTANT: should always match the current + * state of the Controller. + */ + private void resizePinnedStack(@State int state) { + if (state != mState) { + throw new IllegalArgumentException("The passed state should match the current state!"); + } + if (DEBUG) Log.d(TAG, "resizePinnedStack() state=" + stateToName(mState)); + + final Rect newBounds; + switch (mState) { + case STATE_PIP_MENU: + newBounds = mPipBoundsState.getExpandedBounds(); + break; + + case STATE_PIP: + // Let PipBoundsAlgorithm figure out what the correct bounds are at the moment. + // Internally, it will get the "default" bounds from PipBoundsState and adjust them + // as needed to account for things like IME state (will query PipBoundsState for + // this information as well, so it's important to keep PipBoundsState up to date). + newBounds = mPipBoundsAlgorithm.getNormalBounds(); + break; + + case STATE_NO_PIP: + default: + return; + } + + mPipTaskOrganizer.scheduleAnimateResizePip(newBounds, mResizeAnimationDuration, null); + } + + private void registerSessionListenerForCurrentUser() { + mPipMediaController.registerSessionListenerForCurrentUser(); + } + + private void checkIfPinnedTaskAppeared() { + final TaskInfo pinnedTask = getPinnedTaskInfo(); + if (DEBUG) Log.d(TAG, "checkIfPinnedTaskAppeared(), task=" + pinnedTask); + if (pinnedTask == null) return; + mPinnedTaskId = pinnedTask.taskId; + setState(STATE_PIP); + + mPipMediaController.onActivityPinned(); + mPipNotificationController.show(pinnedTask.topActivity.getPackageName()); + } + + private void checkIfPinnedTaskIsGone() { + if (DEBUG) Log.d(TAG, "onTaskStackChanged()"); + + if (isPipShown() && getPinnedTaskInfo() == null) { + Log.w(TAG, "Pinned task is gone."); + onPipDisappeared(); + } + } + + private void onPipDisappeared() { + if (DEBUG) Log.d(TAG, "onPipDisappeared() state=" + stateToName(mState)); + + mPipNotificationController.dismiss(); + mTvPipMenuController.hideMenu(); + setState(STATE_NO_PIP); + mPinnedTaskId = NONEXISTENT_TASK_ID; + } + + @Override + public void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds) { + if (DEBUG) Log.d(TAG, "onPipTransition_Started(), state=" + stateToName(mState)); + } + + @Override + public void onPipTransitionCanceled(ComponentName activity, int direction) { + if (DEBUG) Log.d(TAG, "onPipTransition_Canceled(), state=" + stateToName(mState)); + } + + @Override + public void onPipTransitionFinished(ComponentName activity, int direction) { + if (DEBUG) Log.d(TAG, "onPipTransition_Finished(), state=" + stateToName(mState)); + + if (mState == STATE_PIP_MENU) { + if (DEBUG) Log.d(TAG, " > show menu"); + mTvPipMenuController.showMenu(); + } + } + + private void setState(@State int state) { + if (DEBUG) { + Log.d(TAG, "setState(), state=" + stateToName(state) + ", prev=" + + stateToName(mState)); + } + mState = state; + } + + private void loadConfigurations() { + final Resources res = mContext.getResources(); + mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration); + // "Cache" bounds for the Pip menu as "expanded" bounds in PipBoundsState. We'll refer back + // to this value in resizePinnedStack(), when we are adjusting Pip task/window position for + // the menu. + mPipBoundsState.setExpandedBounds( + Rect.unflattenFromString(res.getString(R.string.pip_menu_bounds))); + } + + private DisplayInfo getDisplayInfo() { + final DisplayInfo displayInfo = new DisplayInfo(); + mContext.getDisplay().getDisplayInfo(displayInfo); + return displayInfo; + } + + private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) { + taskStackListener.addListener(new TaskStackListenerCallback() { + @Override + public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { + checkIfPinnedTaskAppeared(); + } + + @Override + public void onTaskStackChanged() { + checkIfPinnedTaskIsGone(); + } + + @Override + public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, + boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { + if (task.getWindowingMode() == WINDOWING_MODE_PINNED) { + if (DEBUG) Log.d(TAG, "onPinnedActivityRestartAttempt()"); + + // If the "Pip-ed" Activity is launched again by Launcher or intent, make it + // fullscreen. + movePipToFullscreen(); + } + } + }); + } + + private void registerWmShellPinnedStackListener(WindowManagerShellWrapper wmShell) { + try { + wmShell.addPinnedStackListener(new PinnedStackListenerForwarder.PinnedStackListener() { + @Override + public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { + if (DEBUG) { + Log.d(TAG, "onImeVisibilityChanged(), visible=" + imeVisible + + ", height=" + imeHeight); + } + + if (imeVisible == mPipBoundsState.isImeShowing() + && (!imeVisible || imeHeight == mPipBoundsState.getImeHeight())) { + // Nothing changed: either IME has been and remains invisible, or remains + // visible with the same height. + return; + } + mPipBoundsState.setImeVisibility(imeVisible, imeHeight); + // "Normal" Pip bounds may have changed, so if we are in the "normal" state, + // let's update the bounds. + if (mState == STATE_PIP) { + resizePinnedStack(STATE_PIP); + } + } + + @Override + public void onMovementBoundsChanged(boolean fromImeAdjustment) {} + + @Override + public void onActionsChanged(ParceledListSlice<RemoteAction> actions) { + if (DEBUG) Log.d(TAG, "onActionsChanged()"); + + mTvPipMenuController.setAppActions(actions); + } + }); + } catch (RemoteException e) { + Log.e(TAG, "Failed to register pinned stack listener", e); + } + } + + private static TaskInfo getPinnedTaskInfo() { + if (DEBUG) Log.d(TAG, "getPinnedTaskInfo()"); + try { + final TaskInfo taskInfo = ActivityTaskManager.getService().getRootTaskInfo( + WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); + if (DEBUG) Log.d(TAG, " > taskInfo=" + taskInfo); + return taskInfo; + } catch (RemoteException e) { + Log.e(TAG, "getRootTaskInfo() failed", e); + return null; + } + } + + private static void removeTask(int taskId) { + if (DEBUG) Log.d(TAG, "removeTask(), taskId=" + taskId); + try { + ActivityTaskManager.getService().removeTask(taskId); + } catch (Exception e) { + Log.e(TAG, "Atm.removeTask() failed", e); + } + } + + private static String stateToName(@State int state) { + switch (state) { + case STATE_NO_PIP: + return "NO_PIP"; + case STATE_PIP: + return "PIP"; + case STATE_PIP_MENU: + return "PIP_MENU"; + default: + // This can't happen. + throw new IllegalArgumentException("Unknown state " + state); + } + } + + private class TvPipImpl implements Pip { + @Override + public void onConfigurationChanged(Configuration newConfig) { + mMainExecutor.execute(() -> { + TvPipController.this.onConfigurationChanged(newConfig); + }); + } + + @Override + public void registerSessionListenerForCurrentUser() { + mMainExecutor.execute(() -> { + TvPipController.this.registerSessionListenerForCurrentUser(); + }); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuActionButton.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuActionButton.java new file mode 100644 index 000000000000..6f7cd82f8da0 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuActionButton.java @@ -0,0 +1,197 @@ +/* + * 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 that represents Pip Menu action button, such as "Fullscreen" and "Close" as well custom + * (provided by the application in Pip) and media buttons. + */ +public class TvPipMenuActionButton extends RelativeLayout implements View.OnClickListener { + private final ImageView mIconImageView; + private final ImageView mButtonImageView; + private final TextView mDescriptionTextView; + private Animator mTextFocusGainAnimator; + private Animator mButtonFocusGainAnimator; + private Animator mTextFocusLossAnimator; + private Animator mButtonFocusLossAnimator; + private OnClickListener mOnClickListener; + + public TvPipMenuActionButton(Context context) { + this(context, null, 0, 0); + } + + public TvPipMenuActionButton(Context context, AttributeSet attrs) { + this(context, attrs, 0, 0); + } + + public TvPipMenuActionButton(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public TvPipMenuActionButton( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + final LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.tv_pip_menu_action_button, this); + + mIconImageView = findViewById(R.id.icon); + mButtonImageView = findViewById(R.id.button); + mDescriptionTextView = findViewById(R.id.desc); + + final int[] values = new int[]{android.R.attr.src, android.R.attr.text}; + final TypedArray typedArray = context.obtainStyledAttributes(attrs, values, defStyleAttr, + defStyleRes); + + setImageResource(typedArray.getResourceId(0, 0)); + final int textResId = typedArray.getResourceId(1, 0); + if (textResId != 0) { + setTextAndDescription(getContext().getString(textResId)); + } + + typedArray.recycle(); + } + + @Override + public void onFinishInflate() { + super.onFinishInflate(); + mButtonImageView.setOnFocusChangeListener((v, hasFocus) -> { + if (hasFocus) { + startFocusGainAnimation(); + } else { + startFocusLossAnimation(); + } + }); + + 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) { + // We do not want to set an OnClickListener to the TvPipMenuActionButton itself, but only to + // the ImageView. So let's "cash" the listener we've been passed here and set a "proxy" + // listener to the ImageView. + mOnClickListener = listener; + mButtonImageView.setOnClickListener(listener != null ? this : null); + } + + @Override + public void onClick(View v) { + if (mOnClickListener != null) { + // Pass the correct view - this. + mOnClickListener.onClick(this); + } + } + + /** + * 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 setTextAndDescription(CharSequence text) { + mButtonImageView.setContentDescription(text); + mDescriptionTextView.setText(text); + } + + 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. + // However, 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/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java new file mode 100644 index 000000000000..ee41b41a743d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -0,0 +1,238 @@ +/* + * 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.view.WindowManager.SHELL_ROOT_LAYER_PIP; + +import android.app.RemoteAction; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ParceledListSlice; +import android.os.Handler; +import android.util.Log; +import android.view.SurfaceControl; + +import androidx.annotation.Nullable; + +import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipMediaController; +import com.android.wm.shell.pip.PipMenuController; + +import java.util.ArrayList; +import java.util.List; + +/** + * Manages the visibility of the PiP Menu as user interacts with PiP. + */ +public class TvPipMenuController implements PipMenuController, TvPipMenuView.Listener { + private static final String TAG = "TvPipMenuController"; + private static final boolean DEBUG = TvPipController.DEBUG; + + private final Context mContext; + private final SystemWindows mSystemWindows; + private final PipBoundsState mPipBoundsState; + private final Handler mMainHandler; + + private Delegate mDelegate; + private SurfaceControl mLeash; + private TvPipMenuView mMenuView; + + private final List<RemoteAction> mMediaActions = new ArrayList<>(); + private final List<RemoteAction> mAppActions = new ArrayList<>(); + + public TvPipMenuController(Context context, PipBoundsState pipBoundsState, + SystemWindows systemWindows, PipMediaController pipMediaController, + Handler mainHandler) { + mContext = context; + mPipBoundsState = pipBoundsState; + mSystemWindows = systemWindows; + mMainHandler = mainHandler; + + // We need to "close" the menu the platform call for all the system dialogs to close (for + // example, on the Home button press). + final BroadcastReceiver closeSystemDialogsBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + hideMenu(); + } + }; + context.registerReceiverForAllUsers(closeSystemDialogsBroadcastReceiver, + new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), null /* permission */, + mainHandler); + + pipMediaController.addActionListener(this::onMediaActionsChanged); + } + + void setDelegate(Delegate delegate) { + if (DEBUG) Log.d(TAG, "setDelegate(), delegate=" + delegate); + if (mDelegate != null) { + throw new IllegalStateException( + "The delegate has already been set and should not change."); + } + if (delegate == null) { + throw new IllegalArgumentException("The delegate must not be null."); + } + + mDelegate = delegate; + } + + @Override + public void attach(SurfaceControl leash) { + if (mDelegate == null) { + throw new IllegalStateException("Delegate is not set."); + } + + mLeash = leash; + attachPipMenuView(); + } + + private void attachPipMenuView() { + if (DEBUG) Log.d(TAG, "attachPipMenuView()"); + + if (mMenuView != null) { + detachPipMenuView(); + } + + mMenuView = new TvPipMenuView(mContext); + mMenuView.setListener(this); + mSystemWindows.addView(mMenuView, + getPipMenuLayoutParams(MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */), + 0, SHELL_ROOT_LAYER_PIP); + } + + @Override + public void showMenu() { + if (DEBUG) Log.d(TAG, "showMenu()"); + + if (mMenuView != null) { + mSystemWindows.updateViewLayout(mMenuView, getPipMenuLayoutParams(MENU_WINDOW_TITLE, + mPipBoundsState.getDisplayBounds().width(), + mPipBoundsState.getDisplayBounds().height())); + maybeUpdateMenuViewActions(); + mMenuView.show(); + + // By default, SystemWindows views are above everything else. + // Set the relative z-order so the menu is below PiP. + if (mMenuView.getWindowSurfaceControl() != null && mLeash != null) { + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + t.setRelativeLayer(mMenuView.getWindowSurfaceControl(), mLeash, -1); + t.apply(); + } + } + } + + void hideMenu() { + hideMenu(true); + } + + void hideMenu(boolean movePipWindow) { + if (DEBUG) Log.d(TAG, "hideMenu(), movePipWindow=" + movePipWindow); + + if (!isMenuVisible()) { + return; + } + + mMenuView.hide(); + if (movePipWindow) { + mDelegate.movePipToNormalPosition(); + } + } + + @Override + public void detach() { + hideMenu(); + detachPipMenuView(); + mLeash = null; + } + + private void detachPipMenuView() { + if (DEBUG) Log.d(TAG, "detachPipMenuView()"); + + if (mMenuView == null) { + return; + } + + mSystemWindows.removeView(mMenuView); + mMenuView = null; + } + + @Override + public void setAppActions(ParceledListSlice<RemoteAction> actions) { + if (DEBUG) Log.d(TAG, "setAppActions()"); + updateAdditionalActionsList(mAppActions, actions.getList()); + } + + private void onMediaActionsChanged(List<RemoteAction> actions) { + if (DEBUG) Log.d(TAG, "onMediaActionsChanged()"); + updateAdditionalActionsList(mMediaActions, actions); + } + + private void updateAdditionalActionsList( + List<RemoteAction> destination, @Nullable List<RemoteAction> source) { + final int number = source != null ? source.size() : 0; + if (number == 0 && destination.isEmpty()) { + // Nothing changed. + return; + } + + destination.clear(); + if (number > 0) { + destination.addAll(source); + } + maybeUpdateMenuViewActions(); + } + + private void maybeUpdateMenuViewActions() { + if (mMenuView == null) { + return; + } + if (!mAppActions.isEmpty()) { + mMenuView.setAdditionalActions(mAppActions, mMainHandler); + } else { + mMenuView.setAdditionalActions(mMediaActions, mMainHandler); + } + } + + @Override + public boolean isMenuVisible() { + return mMenuView != null && mMenuView.isVisible(); + } + + @Override + public void onBackPress() { + hideMenu(); + } + + @Override + public void onCloseButtonClick() { + mDelegate.closePip(); + } + + @Override + public void onFullscreenButtonClick() { + mDelegate.movePipToFullscreen(); + } + + interface Delegate { + void movePipToNormalPosition(); + void movePipToFullscreen(); + void closePip(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java new file mode 100644 index 000000000000..d6cd9ea13ca1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java @@ -0,0 +1,233 @@ +/* + * 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.animation.AnimatorInflater.loadAnimator; +import static android.view.KeyEvent.ACTION_UP; +import static android.view.KeyEvent.KEYCODE_BACK; + +import android.animation.Animator; +import android.app.PendingIntent; +import android.app.RemoteAction; +import android.content.Context; +import android.graphics.Color; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.SurfaceControl; +import android.view.View; +import android.view.ViewRootImpl; +import android.view.WindowManagerGlobal; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.wm.shell.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * A View that represents Pip Menu on TV. It's responsible for displaying 2 ever-present Pip Menu + * actions: Fullscreen and Close, but could also display "additional" actions, that may be set via + * a {@link #setAdditionalActions(List, Handler)} call. + */ +public class TvPipMenuView extends FrameLayout implements View.OnClickListener { + private static final String TAG = "TvPipMenuView"; + private static final boolean DEBUG = TvPipController.DEBUG; + + private static final float DISABLED_ACTION_ALPHA = 0.54f; + + private final Animator mFadeInAnimation; + private final Animator mFadeOutAnimation; + @Nullable private Listener mListener; + + private final LinearLayout mActionButtonsContainer; + private final List<TvPipMenuActionButton> mAdditionalButtons = new ArrayList<>(); + + public TvPipMenuView(@NonNull Context context) { + this(context, null); + } + + public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + inflate(context, R.layout.tv_pip_menu, this); + + mActionButtonsContainer = findViewById(R.id.tv_pip_menu_action_buttons); + mActionButtonsContainer.findViewById(R.id.tv_pip_menu_fullscreen_button) + .setOnClickListener(this); + mActionButtonsContainer.findViewById(R.id.tv_pip_menu_close_button) + .setOnClickListener(this); + + mFadeInAnimation = loadAnimator(mContext, R.anim.tv_pip_menu_fade_in_animation); + mFadeInAnimation.setTarget(mActionButtonsContainer); + + mFadeOutAnimation = loadAnimator(mContext, R.anim.tv_pip_menu_fade_out_animation); + mFadeOutAnimation.setTarget(mActionButtonsContainer); + } + + void setListener(@Nullable Listener listener) { + mListener = listener; + } + + void show() { + if (DEBUG) Log.d(TAG, "show()"); + + mFadeInAnimation.start(); + setAlpha(1.0f); + grantWindowFocus(true); + } + + void hide() { + if (DEBUG) Log.d(TAG, "hide()"); + + mFadeOutAnimation.start(); + setAlpha(0.0f); + grantWindowFocus(false); + } + + boolean isVisible() { + return getAlpha() == 1.0f; + } + + private void grantWindowFocus(boolean grantFocus) { + if (DEBUG) Log.d(TAG, "grantWindowFocus(" + grantFocus + ")"); + + try { + WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */, + getViewRootImpl().getInputToken(), grantFocus); + } catch (Exception e) { + Log.e(TAG, "Unable to update focus", e); + } + } + + void setAdditionalActions(List<RemoteAction> actions, Handler mainHandler) { + if (DEBUG) Log.d(TAG, "setAdditionalActions()"); + + // Make sure we exactly as many additional buttons as we have actions to display. + final int actionsNumber = actions.size(); + int buttonsNumber = mAdditionalButtons.size(); + if (actionsNumber > buttonsNumber) { + final LayoutInflater layoutInflater = LayoutInflater.from(mContext); + // Add buttons until we have enough to display all of the actions. + while (actionsNumber > buttonsNumber) { + final TvPipMenuActionButton button = (TvPipMenuActionButton) layoutInflater.inflate( + R.layout.tv_pip_menu_additional_action_button, mActionButtonsContainer, + false); + button.setOnClickListener(this); + + mActionButtonsContainer.addView(button); + mAdditionalButtons.add(button); + + buttonsNumber++; + } + } else if (actionsNumber < buttonsNumber) { + // Hide buttons until we as many as the actions. + while (actionsNumber < buttonsNumber) { + final View button = mAdditionalButtons.get(buttonsNumber - 1); + button.setVisibility(View.GONE); + button.setTag(null); + + buttonsNumber--; + } + } + + // "Assign" actions to the buttons. + for (int index = 0; index < actionsNumber; index++) { + final RemoteAction action = actions.get(index); + final TvPipMenuActionButton button = mAdditionalButtons.get(index); + button.setVisibility(View.VISIBLE); // Ensure the button is visible. + button.setTextAndDescription(action.getContentDescription()); + button.setEnabled(action.isEnabled()); + button.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA); + button.setTag(action); + + action.getIcon().loadDrawableAsync(mContext, drawable -> { + drawable.setTint(Color.WHITE); + button.setImageDrawable(drawable); + }, mainHandler); + } + } + + @Nullable + SurfaceControl getWindowSurfaceControl() { + final ViewRootImpl root = getViewRootImpl(); + if (root == null) { + return null; + } + final SurfaceControl out = root.getSurfaceControl(); + if (out != null && out.isValid()) { + return out; + } + return null; + } + + @Override + public void onClick(View v) { + if (mListener == null) return; + + final int id = v.getId(); + if (id == R.id.tv_pip_menu_fullscreen_button) { + mListener.onFullscreenButtonClick(); + } else if (id == R.id.tv_pip_menu_close_button) { + mListener.onCloseButtonClick(); + } else { + // This should be an "additional action" + final RemoteAction action = (RemoteAction) v.getTag(); + if (action != null) { + try { + action.getActionIntent().send(); + } catch (PendingIntent.CanceledException e) { + Log.w(TAG, "Failed to send action", e); + } + } else { + Log.w(TAG, "RemoteAction is null"); + } + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getAction() == ACTION_UP && event.getKeyCode() == KEYCODE_BACK + && mListener != null) { + mListener.onBackPress(); + return true; + } + return super.dispatchKeyEvent(event); + } + + interface Listener { + void onBackPress(); + void onCloseButtonClick(); + void onFullscreenButtonClick(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java new file mode 100644 index 000000000000..a47483144fef --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java @@ -0,0 +1,255 @@ +/* + * 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.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.media.MediaMetadata; +import android.os.Handler; +import android.os.UserHandle; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; +import com.android.wm.shell.R; +import com.android.wm.shell.pip.PipMediaController; + +import java.util.Objects; + +/** + * 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 TvPipNotificationController { + private static final String TAG = "TvPipNotification"; + private static final boolean DEBUG = TvPipController.DEBUG; + + // Referenced in com.android.systemui.util.NotificationChannels. + public static final String NOTIFICATION_CHANNEL = "TVPIP"; + private static final String NOTIFICATION_TAG = "TvPip"; + + private static final String ACTION_SHOW_PIP_MENU = + "com.android.wm.shell.pip.tv.notification.action.SHOW_PIP_MENU"; + private static final String ACTION_CLOSE_PIP = + "com.android.wm.shell.pip.tv.notification.action.CLOSE_PIP"; + + private final Context mContext; + private final PackageManager mPackageManager; + private final NotificationManager mNotificationManager; + private final Notification.Builder mNotificationBuilder; + private final ActionBroadcastReceiver mActionBroadcastReceiver; + private final Handler mMainHandler; + private Delegate mDelegate; + + private String mDefaultTitle; + + /** Package name for the application that owns PiP window. */ + private String mPackageName; + private boolean mNotified; + private String mMediaTitle; + private Bitmap mArt; + + public TvPipNotificationController(Context context, PipMediaController pipMediaController, + Handler mainHandler) { + mContext = context; + mPackageManager = context.getPackageManager(); + mNotificationManager = context.getSystemService(NotificationManager.class); + mMainHandler = mainHandler; + + mNotificationBuilder = new Notification.Builder(context, NOTIFICATION_CHANNEL) + .setLocalOnly(true) + .setOngoing(false) + .setCategory(Notification.CATEGORY_SYSTEM) + .setShowWhen(true) + .setSmallIcon(R.drawable.pip_icon) + .extend(new Notification.TvExtender() + .setContentIntent(createPendingIntent(context, ACTION_SHOW_PIP_MENU)) + .setDeleteIntent(createPendingIntent(context, ACTION_CLOSE_PIP))); + + mActionBroadcastReceiver = new ActionBroadcastReceiver(); + + pipMediaController.addMetadataListener(this::onMediaMetadataChanged); + + onConfigurationChanged(context); + } + + void setDelegate(Delegate delegate) { + if (DEBUG) Log.d(TAG, "setDelegate(), delegate=" + delegate); + if (mDelegate != null) { + throw new IllegalStateException( + "The delegate has already been set and should not change."); + } + if (delegate == null) { + throw new IllegalArgumentException("The delegate must not be null."); + } + + mDelegate = delegate; + } + + void show(String packageName) { + if (mDelegate == null) { + throw new IllegalStateException("Delegate is not set."); + } + + mPackageName = packageName; + update(); + mActionBroadcastReceiver.register(); + } + + void dismiss() { + mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP); + mNotified = false; + mPackageName = null; + mActionBroadcastReceiver.unregister(); + } + + private void onMediaMetadataChanged(MediaMetadata metadata) { + if (updateMediaControllerMetadata(metadata) && mNotified) { + // update notification + update(); + } + } + + /** + * Called by {@link PipController} when the configuration is changed. + */ + void onConfigurationChanged(Context context) { + mDefaultTitle = context.getResources().getString(R.string.pip_notification_unknown_title); + if (mNotified) { + // Update the notification. + update(); + } + } + + private void update() { + mNotified = true; + mNotificationBuilder + .setWhen(System.currentTimeMillis()) + .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 boolean updateMediaControllerMetadata(MediaMetadata metadata) { + String title = null; + Bitmap art = null; + 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) && Objects.equals(art, mArt)) { + return false; + } + + mMediaTitle = title; + mArt = art; + + return true; + } + + + 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 | PendingIntent.FLAG_IMMUTABLE); + } + + private class ActionBroadcastReceiver extends BroadcastReceiver { + final IntentFilter mIntentFilter; + { + mIntentFilter = new IntentFilter(); + mIntentFilter.addAction(ACTION_CLOSE_PIP); + mIntentFilter.addAction(ACTION_SHOW_PIP_MENU); + } + boolean mRegistered = false; + + void register() { + if (mRegistered) return; + + mContext.registerReceiverForAllUsers(this, mIntentFilter, null /* permission */, + mMainHandler); + mRegistered = true; + } + + void unregister() { + if (!mRegistered) return; + + mContext.unregisterReceiver(this); + mRegistered = false; + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (DEBUG) Log.d(TAG, "on(Broadcast)Receive(), action=" + action); + + if (ACTION_SHOW_PIP_MENU.equals(action)) { + mDelegate.showPictureInPictureMenu(); + } else if (ACTION_CLOSE_PIP.equals(action)) { + mDelegate.closePip(); + } + } + } + + interface Delegate { + void showPictureInPictureMenu(); + void closePip(); + } +} 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..4f4e7dafe5c0 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java @@ -0,0 +1,98 @@ +/* + * 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), + WM_SHELL_DRAG_AND_DROP(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM_SHELL), + TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest"); + + private final boolean mEnabled; + private volatile boolean mLogToProto; + private volatile boolean mLogToLogcat; + private final String mTag; + + /** + * @param enabled set to false to exclude all log statements for this group from + * compilation, + * they will not be available in runtime. + * @param logToProto enable binary logging for the group + * @param logToLogcat enable text logging for the group + * @param tag name of the source of the logged message + */ + ShellProtoLogGroup(boolean enabled, boolean logToProto, boolean logToLogcat, String tag) { + this.mEnabled = enabled; + this.mLogToProto = logToProto; + this.mLogToLogcat = logToLogcat; + this.mTag = tag; + } + + @Override + public boolean isEnabled() { + return mEnabled; + } + + @Override + public boolean isLogToProto() { + return mLogToProto; + } + + @Override + public boolean isLogToLogcat() { + return mLogToLogcat; + } + + @Override + public boolean isLogToAny() { + return mLogToLogcat || mLogToProto; + } + + @Override + public String getTag() { + return mTag; + } + + @Override + public void setLogToProto(boolean logToProto) { + this.mLogToProto = logToProto; + } + + @Override + public void setLogToLogcat(boolean logToLogcat) { + this.mLogToLogcat = logToLogcat; + } + + private static class Consts { + private static final String TAG_WM_SHELL = "WindowManagerShell"; + + private static final boolean ENABLE_DEBUG = true; + private static final boolean ENABLE_LOG_TO_PROTO_DEBUG = true; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java new file mode 100644 index 000000000000..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/sizecompatui/SizeCompatRestartButton.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButton.java new file mode 100644 index 000000000000..e47e1ac71c73 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButton.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.sizecompatui; + +import android.app.ActivityClient; +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.RippleDrawable; +import android.os.IBinder; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.PopupWindow; + +import com.android.wm.shell.R; + +/** Button to restart the size compat activity. */ +class SizeCompatRestartButton extends ImageButton implements View.OnClickListener, + View.OnLongClickListener { + private static final String TAG = "SizeCompatRestartButton"; + + final WindowManager.LayoutParams mWinParams; + final boolean mShouldShowHint; + final int mDisplayId; + final int mPopupOffsetX; + final int mPopupOffsetY; + + private IBinder mLastActivityToken; + private PopupWindow mShowingHint; + + SizeCompatRestartButton(Context context, int displayId, boolean hasShownHint) { + super(context); + mDisplayId = displayId; + mShouldShowHint = !hasShownHint; + final Drawable drawable = context.getDrawable(R.drawable.size_compat_restart_button); + setImageDrawable(drawable); + setContentDescription(context.getString(R.string.restart_button_description)); + + final int drawableW = drawable.getIntrinsicWidth(); + final int drawableH = drawable.getIntrinsicHeight(); + mPopupOffsetX = drawableW / 2; + mPopupOffsetY = drawableH * 2; + + final ColorStateList color = ColorStateList.valueOf(Color.LTGRAY); + final GradientDrawable mask = new GradientDrawable(); + mask.setShape(GradientDrawable.OVAL); + mask.setColor(color); + setBackground(new RippleDrawable(color, null /* content */, mask)); + setOnClickListener(this); + setOnLongClickListener(this); + + mWinParams = new WindowManager.LayoutParams(); + mWinParams.gravity = getGravity(getResources().getConfiguration().getLayoutDirection()); + mWinParams.width = drawableW * 2; + mWinParams.height = drawableH * 2; + mWinParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + mWinParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; + mWinParams.format = PixelFormat.TRANSLUCENT; + mWinParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + mWinParams.setTitle(SizeCompatRestartButton.class.getSimpleName() + + context.getDisplayId()); + } + + void updateLastTargetActivity(IBinder activityToken) { + mLastActivityToken = activityToken; + } + + /** @return {@code false} if the target display is invalid. */ + boolean show() { + try { + getContext().getSystemService(WindowManager.class).addView(this, mWinParams); + } catch (WindowManager.InvalidDisplayException e) { + // The target display may have been removed when the callback has just arrived. + Log.w(TAG, "Cannot show on display " + getContext().getDisplayId(), e); + return false; + } + return true; + } + + void remove() { + if (mShowingHint != null) { + mShowingHint.dismiss(); + } + getContext().getSystemService(WindowManager.class).removeViewImmediate(this); + } + + @Override + public void onClick(View v) { + ActivityClient.getInstance().restartActivityProcessIfVisible(mLastActivityToken); + } + + @Override + public boolean onLongClick(View v) { + showHint(); + return true; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (mShouldShowHint) { + showHint(); + } + } + + @Override + public void setLayoutDirection(int layoutDirection) { + final int gravity = getGravity(layoutDirection); + if (mWinParams.gravity != gravity) { + mWinParams.gravity = gravity; + if (mShowingHint != null) { + mShowingHint.dismiss(); + showHint(); + } + getContext().getSystemService(WindowManager.class).updateViewLayout(this, + mWinParams); + } + super.setLayoutDirection(layoutDirection); + } + + void showHint() { + if (mShowingHint != null) { + return; + } + + final View popupView = LayoutInflater.from(getContext()).inflate( + R.layout.size_compat_mode_hint, null /* root */); + final PopupWindow popupWindow = new PopupWindow(popupView, + LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); + popupWindow.setWindowLayoutType(mWinParams.type); + popupWindow.setElevation(getResources().getDimension(R.dimen.bubble_elevation)); + popupWindow.setAnimationStyle(android.R.style.Animation_InputMethod); + popupWindow.setClippingEnabled(false); + popupWindow.setOnDismissListener(() -> mShowingHint = null); + mShowingHint = popupWindow; + + final Button gotItButton = popupView.findViewById(R.id.got_it); + gotItButton.setBackground(new RippleDrawable(ColorStateList.valueOf(Color.LTGRAY), + null /* content */, null /* mask */)); + gotItButton.setOnClickListener(view -> popupWindow.dismiss()); + popupWindow.showAtLocation(this, mWinParams.gravity, mPopupOffsetX, mPopupOffsetY); + } + + private static int getGravity(int layoutDirection) { + return Gravity.BOTTOM + | (layoutDirection == View.LAYOUT_DIRECTION_RTL ? Gravity.START : Gravity.END); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUI.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUI.java new file mode 100644 index 000000000000..11f22ed24a69 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUI.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.sizecompatui; + +import android.annotation.Nullable; +import android.graphics.Rect; +import android.os.IBinder; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.annotations.ExternalThread; + +/** + * Interface to engage size compat mode UI. + */ +@ExternalThread +public interface SizeCompatUI { + /** + * Called when the Task info changed. Creates and updates the restart button if there is an + * activity in size compat, or removes the restart button if there is no size compat activity. + * + * @param displayId display the task and activity are in. + * @param taskId task the activity is in. + * @param taskBounds task bounds to place the restart button in. + * @param sizeCompatActivity the size compat activity in the task. Can be {@code null} if the + * top activity in this Task is not in size compat. + * @param taskListener listener to handle the Task Surface placement. + */ + void onSizeCompatInfoChanged(int displayId, int taskId, @Nullable Rect taskBounds, + @Nullable IBinder sizeCompatActivity, + @Nullable ShellTaskOrganizer.TaskListener taskListener); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java new file mode 100644 index 000000000000..286c3b6a051e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.sizecompatui; + +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.os.IBinder; +import android.util.Log; +import android.util.SparseArray; +import android.view.Display; +import android.view.View; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.ShellExecutor; + +import java.lang.ref.WeakReference; + +/** + * Shows a restart-activity button on Task when the foreground activity is in size compatibility + * mode. + */ +public class SizeCompatUIController implements DisplayController.OnDisplaysChangedListener, + DisplayImeController.ImePositionProcessor { + private static final String TAG = "SizeCompatUI"; + + /** The showing buttons by task id. */ + private final SparseArray<SizeCompatRestartButton> mActiveButtons = new SparseArray<>(1); + /** Avoid creating display context frequently for non-default display. */ + private final SparseArray<WeakReference<Context>> mDisplayContextCache = new SparseArray<>(0); + + @VisibleForTesting + final SizeCompatUI mImpl = new SizeCompatUIImpl(); + private final Context mContext; + private final ShellExecutor mMainExecutor; + private final DisplayController mDisplayController; + private final DisplayImeController mImeController; + + /** Only show once automatically in the process life. */ + private boolean mHasShownHint; + + /** Creates the {@link SizeCompatUIController}. */ + public static SizeCompatUI create(Context context, + DisplayController displayController, + DisplayImeController imeController, + ShellExecutor mainExecutor) { + return new SizeCompatUIController(context, displayController, imeController, mainExecutor) + .mImpl; + } + + @VisibleForTesting + SizeCompatUIController(Context context, + DisplayController displayController, + DisplayImeController imeController, + ShellExecutor mainExecutor) { + mContext = context; + mMainExecutor = mainExecutor; + mDisplayController = displayController; + mImeController = imeController; + mDisplayController.addDisplayWindowListener(this); + mImeController.addPositionProcessor(this); + } + + private void onSizeCompatInfoChanged(int displayId, int taskId, @Nullable Rect taskBounds, + @Nullable IBinder sizeCompatActivity, + @Nullable ShellTaskOrganizer.TaskListener taskListener) { + // TODO Draw button on Task surface + if (taskBounds == null || sizeCompatActivity == null || taskListener == null) { + // Null token means the current foreground activity is not in size compatibility mode. + removeRestartButton(taskId); + } else { + updateRestartButton(displayId, taskId, sizeCompatActivity); + } + } + + @Override + public void onDisplayRemoved(int displayId) { + mDisplayContextCache.remove(displayId); + for (int i = 0; i < mActiveButtons.size(); i++) { + final int taskId = mActiveButtons.keyAt(i); + final SizeCompatRestartButton button = mActiveButtons.get(taskId); + if (button != null && button.mDisplayId == displayId) { + removeRestartButton(taskId); + } + } + } + + @Override + public void onImeVisibilityChanged(int displayId, boolean isShowing) { + final int newVisibility = isShowing ? View.GONE : View.VISIBLE; + for (int i = 0; i < mActiveButtons.size(); i++) { + final int taskId = mActiveButtons.keyAt(i); + final SizeCompatRestartButton button = mActiveButtons.get(taskId); + if (button == null || button.mDisplayId != displayId) { + continue; + } + + // Hide the button when input method is showing. + if (button.getVisibility() != newVisibility) { + button.setVisibility(newVisibility); + } + } + } + + private void updateRestartButton(int displayId, int taskId, IBinder activityToken) { + SizeCompatRestartButton restartButton = mActiveButtons.get(taskId); + if (restartButton != null) { + restartButton.updateLastTargetActivity(activityToken); + return; + } + + final Context context = getOrCreateDisplayContext(displayId); + if (context == null) { + Log.i(TAG, "Cannot get context for display " + displayId); + return; + } + + restartButton = createRestartButton(context, displayId); + restartButton.updateLastTargetActivity(activityToken); + if (restartButton.show()) { + mActiveButtons.append(taskId, restartButton); + } else { + onDisplayRemoved(displayId); + } + } + + @VisibleForTesting + SizeCompatRestartButton createRestartButton(Context context, int displayId) { + final SizeCompatRestartButton button = new SizeCompatRestartButton(context, displayId, + mHasShownHint); + // Only show hint for the first time. + mHasShownHint = true; + return button; + } + + private void removeRestartButton(int taskId) { + final SizeCompatRestartButton button = mActiveButtons.get(taskId); + if (button != null) { + button.remove(); + mActiveButtons.remove(taskId); + } + } + + private Context getOrCreateDisplayContext(int displayId) { + if (displayId == Display.DEFAULT_DISPLAY) { + return mContext; + } + Context context = null; + final WeakReference<Context> ref = mDisplayContextCache.get(displayId); + if (ref != null) { + context = ref.get(); + } + if (context == null) { + Display display = mContext.getSystemService(DisplayManager.class).getDisplay(displayId); + if (display != null) { + context = mContext.createDisplayContext(display); + mDisplayContextCache.put(displayId, new WeakReference<>(context)); + } + } + return context; + } + + private class SizeCompatUIImpl implements SizeCompatUI { + @Override + public void onSizeCompatInfoChanged(int displayId, int taskId, @Nullable Rect taskBounds, + @Nullable IBinder sizeCompatActivity, + @Nullable ShellTaskOrganizer.TaskListener taskListener) { + mMainExecutor.execute(() -> + SizeCompatUIController.this.onSizeCompatInfoChanged(displayId, taskId, + taskBounds, sizeCompatActivity, taskListener)); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java new file mode 100644 index 000000000000..2f2e325aafad --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java @@ -0,0 +1,101 @@ +/* + * 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_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import android.graphics.Rect; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.SyncTransactionQueue; + +/** + * Main stage for split-screen mode. When split-screen is active all standard activity types launch + * on the main stage, except for task that are explicitly pinned to the {@link SideStage}. + * @see StageCoordinator + */ +class MainStage extends StageTaskListener { + private static final String TAG = MainStage.class.getSimpleName(); + + private boolean mIsActive = false; + + MainStage(ShellTaskOrganizer taskOrganizer, int displayId, + StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue) { + super(taskOrganizer, displayId, callbacks, syncQueue); + } + + boolean isActive() { + return mIsActive; + } + + void activate(Rect rootBounds, WindowContainerTransaction wct) { + if (mIsActive) return; + + final WindowContainerToken rootToken = mRootTaskInfo.token; + wct.setBounds(rootToken, rootBounds) + .setLaunchRoot( + rootToken, + CONTROLLED_WINDOWING_MODES, + CONTROLLED_ACTIVITY_TYPES) + .reparentTasks( + null /* currentParent */, + rootToken, + CONTROLLED_WINDOWING_MODES, + CONTROLLED_ACTIVITY_TYPES, + true /* onTop */) + // Moving the root task to top after the child tasks were re-parented , or the root + // task cannot be visible and focused. + .reorder(rootToken, true /* onTop */); + + mIsActive = true; + } + + void deactivate(WindowContainerTransaction wct) { + deactivate(wct, false /* toTop */); + } + + void deactivate(WindowContainerTransaction wct, boolean toTop) { + if (!mIsActive) return; + mIsActive = false; + + if (mRootTaskInfo == null) return; + final WindowContainerToken rootToken = mRootTaskInfo.token; + wct.setLaunchRoot( + rootToken, + null, + null) + .reparentTasks( + rootToken, + null /* newParent */, + CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE, + CONTROLLED_ACTIVITY_TYPES, + toTop) + // We want this re-order to the bottom regardless since we are re-parenting + // all its tasks. + .reorder(rootToken, false /* onTop */); + } + + void updateConfiguration(int windowingMode, Rect bounds, WindowContainerTransaction wct) { + wct.setBounds(mRootTaskInfo.token, bounds) + .setWindowingMode(mRootTaskInfo.token, windowingMode); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java new file mode 100644 index 000000000000..e7cd38fb4bca --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.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.splitscreen; + +import android.app.ActivityManager; +import android.graphics.Rect; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.SyncTransactionQueue; + +/** + * Side stage for split-screen mode. Only tasks that are explicitly pinned to this stage show up + * here. All other task are launch in the {@link MainStage}. + * @see StageCoordinator + */ +class SideStage extends StageTaskListener { + private static final String TAG = SideStage.class.getSimpleName(); + + SideStage(ShellTaskOrganizer taskOrganizer, int displayId, + StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue) { + super(taskOrganizer, displayId, callbacks, syncQueue); + } + + void addTask(ActivityManager.RunningTaskInfo task, Rect rootBounds, + WindowContainerTransaction wct) { + final WindowContainerToken rootToken = mRootTaskInfo.token; + wct.setHidden(rootToken, false) + .setBounds(rootToken, rootBounds) + .reparent(task.token, rootToken, true /* onTop*/) + // Moving the root task to top after the child tasks were repareted , or the root + // task cannot be visible and focused. + .reorder(rootToken, true); + } + + boolean removeAllTasks(WindowContainerTransaction wct, boolean toTop) { + if (mChildrenTaskInfo.size() == 0) return false; + wct.reparentTasks( + mRootTaskInfo.token, + null /* newParent */, + CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE, + CONTROLLED_ACTIVITY_TYPES, + toTop); + return true; + } + + boolean removeTask(int taskId, WindowContainerToken newParent, WindowContainerTransaction wct) { + final ActivityManager.RunningTaskInfo task = mChildrenTaskInfo.get(taskId); + if (task == null) return false; + + wct.setHidden(mRootTaskInfo.token, true) + .reorder(mRootTaskInfo.token, false) + .reparent(task.token, newParent, false /* onTop */); + return true; + } +} 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..2c6809259459 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java @@ -0,0 +1,127 @@ +/* + * 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.annotation.IntDef; +import android.app.ActivityManager; +import android.app.PendingIntent; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.UserHandle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.draganddrop.DragAndDropPolicy; + +import java.io.PrintWriter; + +/** + * Interface to engage split-screen feature. + */ +@ExternalThread +public interface SplitScreen extends DragAndDropPolicy.Starter { + /** + * Stage position isn't specified normally meaning to use what ever it is currently set to. + */ + int STAGE_POSITION_UNDEFINED = -1; + /** + * Specifies that a stage is positioned at the top half of the screen if + * in portrait mode or at the left half of the screen if in landscape mode. + */ + int STAGE_POSITION_TOP_OR_LEFT = 0; + + /** + * Specifies that a stage is positioned at the bottom half of the screen if + * in portrait mode or at the right half of the screen if in landscape mode. + */ + int STAGE_POSITION_BOTTOM_OR_RIGHT = 1; + + @IntDef(prefix = { "STAGE_POSITION_" }, value = { + STAGE_POSITION_UNDEFINED, + STAGE_POSITION_TOP_OR_LEFT, + STAGE_POSITION_BOTTOM_OR_RIGHT + }) + @interface StagePosition {} + + /** + * Stage type isn't specified normally meaning to use what ever the default is. + * E.g. exit split-screen and launch the app in fullscreen. + */ + int STAGE_TYPE_UNDEFINED = -1; + /** + * The main stage type. + * @see MainStage + */ + int STAGE_TYPE_MAIN = 0; + + /** + * The side stage type. + * @see SideStage + */ + int STAGE_TYPE_SIDE = 1; + + @IntDef(prefix = { "STAGE_TYPE_" }, value = { + STAGE_TYPE_UNDEFINED, + STAGE_TYPE_MAIN, + STAGE_TYPE_SIDE + }) + @interface StageType {} + + /** Callback interface for listening to changes in a split-screen stage. */ + interface SplitScreenListener { + void onStagePositionChanged(@StageType int stage, @StagePosition int position); + void onTaskStageChanged(int taskId, @StageType int stage); + } + + /** @return {@code true} if split-screen is currently visible. */ + boolean isSplitScreenVisible(); + /** Moves a task in the side-stage of split-screen. */ + boolean moveToSideStage(int taskId, @StagePosition int sideStagePosition); + /** Moves a task in the side-stage of split-screen. */ + boolean moveToSideStage(ActivityManager.RunningTaskInfo task, + @StagePosition int sideStagePosition); + /** Removes a task from the side-stage of split-screen. */ + boolean removeFromSideStage(int taskId); + /** Sets the position of the side-stage. */ + void setSideStagePosition(@StagePosition int sideStagePosition); + /** Hides the side-stage if it is currently visible. */ + void setSideStageVisibility(boolean visible); + default void enterSplitScreen(int taskId, boolean leftOrTop) { + moveToSideStage(taskId, + leftOrTop ? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT); + } + /** Removes the split-screen stages. */ + void exitSplitScreen(); + /** Gets the stage bounds. */ + void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds); + /** Dumps current status of split-screen. */ + void dump(@NonNull PrintWriter pw, String prefix); + /** Called when the shell organizer has been registered. */ + void onOrganizerRegistered(); + + void registerSplitScreenListener(SplitScreenListener listener); + void unregisterSplitScreenListener(SplitScreenListener listener); + + void startTask(int taskId, + @StageType int stage, @StagePosition int position, @Nullable Bundle options); + void startShortcut(String packageName, String shortcutId, @StageType int stage, + @StagePosition int position, @Nullable Bundle options, UserHandle user); + void startIntent(PendingIntent intent, + @StageType int stage, @StagePosition int position, @Nullable Bundle options); +} 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..18dd53b90ff4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -0,0 +1,227 @@ +/* + * 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.view.Display.DEFAULT_DISPLAY; + +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.PendingIntent; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.pm.LauncherApps; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Slog; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.SyncTransactionQueue; + +import java.io.PrintWriter; + +/** + * Class manages split-screen multitasking mode and implements the main interface + * {@link SplitScreen}. + * @see StageCoordinator + */ +public class SplitScreenController implements SplitScreen { + private static final String TAG = SplitScreenController.class.getSimpleName(); + + private final ShellTaskOrganizer mTaskOrganizer; + private final SyncTransactionQueue mSyncQueue; + private final Context mContext; + private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer; + private StageCoordinator mStageCoordinator; + + public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer, + SyncTransactionQueue syncQueue, Context context, + RootTaskDisplayAreaOrganizer rootTDAOrganizer) { + mTaskOrganizer = shellTaskOrganizer; + mSyncQueue = syncQueue; + mContext = context; + mRootTDAOrganizer = rootTDAOrganizer; + } + + @Override + public void onOrganizerRegistered() { + if (mStageCoordinator == null) { + // TODO: Multi-display + mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, + mRootTDAOrganizer, mTaskOrganizer); + } + } + + @Override + public boolean isSplitScreenVisible() { + return mStageCoordinator.isSplitScreenVisible(); + } + + @Override + public boolean moveToSideStage(int taskId, @StagePosition int sideStagePosition) { + final ActivityManager.RunningTaskInfo task = mTaskOrganizer.getRunningTaskInfo(taskId); + if (task == null) { + throw new IllegalArgumentException("Unknown taskId" + taskId); + } + return moveToSideStage(task, sideStagePosition); + } + + @Override + public boolean moveToSideStage(ActivityManager.RunningTaskInfo task, + @StagePosition int sideStagePosition) { + return mStageCoordinator.moveToSideStage(task, sideStagePosition); + } + + @Override + public boolean removeFromSideStage(int taskId) { + return mStageCoordinator.removeFromSideStage(taskId); + } + + @Override + public void setSideStagePosition(@StagePosition int sideStagePosition) { + mStageCoordinator.setSideStagePosition(sideStagePosition); + } + + @Override + public void setSideStageVisibility(boolean visible) { + mStageCoordinator.setSideStageVisibility(visible); + } + + @Override + public void exitSplitScreen() { + mStageCoordinator.exitSplitScreen(); + } + + @Override + public void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) { + mStageCoordinator.getStageBounds(outTopOrLeftBounds, outBottomOrRightBounds); + } + + @Override + public void registerSplitScreenListener(SplitScreenListener listener) { + mStageCoordinator.registerSplitScreenListener(listener); + } + + @Override + public void unregisterSplitScreenListener(SplitScreenListener listener) { + mStageCoordinator.unregisterSplitScreenListener(listener); + } + + @Override + public void startTask(int taskId, + @StageType int stage, @StagePosition int position, @Nullable Bundle options) { + options = resolveStartStage(stage, position, options); + + try { + ActivityTaskManager.getService().startActivityFromRecents(taskId, options); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to launch task", e); + } + } + + @Override + public void startShortcut(String packageName, String shortcutId, @StageType int stage, + @StagePosition int position, @Nullable Bundle options, UserHandle user) { + options = resolveStartStage(stage, position, options); + + try { + LauncherApps launcherApps = + mContext.getSystemService(LauncherApps.class); + launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */, + options, user); + } catch (ActivityNotFoundException e) { + Slog.e(TAG, "Failed to launch shortcut", e); + } + } + + @Override + public void startIntent(PendingIntent intent, + @StageType int stage, @StagePosition int position, @Nullable Bundle options) { + options = resolveStartStage(stage, position, options); + + try { + intent.send(null, 0, null, null, null, null, options); + } catch (PendingIntent.CanceledException e) { + Slog.e(TAG, "Failed to launch activity", e); + } + } + + private Bundle resolveStartStage(@StageType int stage, @StagePosition int position, + @Nullable Bundle options) { + switch (stage) { + case STAGE_TYPE_UNDEFINED: { + // Use the stage of the specified position is valid. + if (position != STAGE_POSITION_UNDEFINED) { + if (position == mStageCoordinator.getSideStagePosition()) { + options = resolveStartStage(STAGE_TYPE_SIDE, position, options); + } else { + options = resolveStartStage(STAGE_TYPE_MAIN, position, options); + } + } else { + // Exit split-screen and launch fullscreen since stage wasn't specified. + mStageCoordinator.exitSplitScreen(); + } + break; + } + case STAGE_TYPE_SIDE: { + if (position != STAGE_POSITION_UNDEFINED) { + mStageCoordinator.setSideStagePosition(position); + } else { + position = mStageCoordinator.getSideStagePosition(); + } + if (options == null) { + options = new Bundle(); + } + mStageCoordinator.updateActivityOptions(options, position); + break; + } + case STAGE_TYPE_MAIN: { + if (position != STAGE_POSITION_UNDEFINED) { + // Set the side stage opposite of what we want to the main stage. + final int sideStagePosition = position == STAGE_POSITION_TOP_OR_LEFT + ? STAGE_POSITION_BOTTOM_OR_RIGHT : STAGE_POSITION_TOP_OR_LEFT; + mStageCoordinator.setSideStagePosition(sideStagePosition); + } else { + position = mStageCoordinator.getMainStagePosition(); + } + if (options == null) { + options = new Bundle(); + } + mStageCoordinator.updateActivityOptions(options, position); + break; + } + default: + throw new IllegalArgumentException("Unknown stage=" + stage); + } + + return options; + } + + @Override + public void dump(@NonNull PrintWriter pw, String prefix) { + pw.println(prefix + TAG); + if (mStageCoordinator != null) { + mStageCoordinator.dump(pw, prefix); + } + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java new file mode 100644 index 000000000000..176852b148fa --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -0,0 +1,496 @@ +/* + * 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.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; + +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; + +import android.app.ActivityManager; +import android.content.Context; +import android.graphics.Rect; +import android.os.Bundle; +import android.view.SurfaceControl; +import android.window.DisplayAreaInfo; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.split.SplitLayout; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and + * {@link SideStage} stages. + * Some high-level rules: + * - The {@link StageCoordinator} is only considered active if the {@link SideStage} contains at + * least one child task. + * - The {@link MainStage} should only have children if the coordinator is active. + * - The {@link SplitLayout} divider is only visible if both the {@link MainStage} + * and {@link SideStage} are visible. + * - The {@link MainStage} configuration is fullscreen when the {@link SideStage} isn't visible. + * This rules are mostly implemented in {@link #onStageVisibilityChanged(StageListenerImpl)} and + * {@link #onStageHasChildrenChanged(StageListenerImpl).} + */ +class StageCoordinator implements SplitLayout.LayoutChangeListener, + RootTaskDisplayAreaOrganizer.RootTaskDisplayAreaListener { + + private static final String TAG = StageCoordinator.class.getSimpleName(); + + private final MainStage mMainStage; + private final StageListenerImpl mMainStageListener = new StageListenerImpl(); + private final SideStage mSideStage; + private final StageListenerImpl mSideStageListener = new StageListenerImpl(); + private @SplitScreen.StagePosition int mSideStagePosition = STAGE_POSITION_BOTTOM_OR_RIGHT; + + private final int mDisplayId; + private SplitLayout mSplitLayout; + private boolean mDividerVisible; + private final SyncTransactionQueue mSyncQueue; + private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer; + private final ShellTaskOrganizer mTaskOrganizer; + private DisplayAreaInfo mDisplayAreaInfo; + private final Context mContext; + private final List<SplitScreen.SplitScreenListener> mListeners = new ArrayList<>(); + + StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, + RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer) { + mContext = context; + mDisplayId = displayId; + mSyncQueue = syncQueue; + mRootTDAOrganizer = rootTDAOrganizer; + mTaskOrganizer = taskOrganizer; + mMainStage = new MainStage(mTaskOrganizer, mDisplayId, mMainStageListener, mSyncQueue); + mSideStage = new SideStage(mTaskOrganizer, mDisplayId, mSideStageListener, mSyncQueue); + mRootTDAOrganizer.registerListener(displayId, this); + } + + @VisibleForTesting + StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, + RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer, + MainStage mainStage, SideStage sideStage) { + mContext = context; + mDisplayId = displayId; + mSyncQueue = syncQueue; + mRootTDAOrganizer = rootTDAOrganizer; + mTaskOrganizer = taskOrganizer; + mMainStage = mainStage; + mSideStage = sideStage; + mRootTDAOrganizer.registerListener(displayId, this); + } + + boolean isSplitScreenVisible() { + return mSideStageListener.mVisible && mMainStageListener.mVisible; + } + + boolean moveToSideStage(ActivityManager.RunningTaskInfo task, + @SplitScreen.StagePosition int sideStagePosition) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mSideStagePosition = sideStagePosition; + mMainStage.activate(getMainStageBounds(), wct); + mSideStage.addTask(task, getSideStageBounds(), wct); + mTaskOrganizer.applyTransaction(wct); + return true; + } + + boolean removeFromSideStage(int taskId) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + + /** + * {@link MainStage} will be deactivated in {@link #onStageHasChildrenChanged} if the + * {@link SideStage} no longer has children. + */ + final boolean result = mSideStage.removeTask(taskId, + mMainStage.isActive() ? mMainStage.mRootTaskInfo.token : null, + wct); + mTaskOrganizer.applyTransaction(wct); + return result; + } + + @SplitScreen.StagePosition int getSideStagePosition() { + return mSideStagePosition; + } + + @SplitScreen.StagePosition int getMainStagePosition() { + return mSideStagePosition == STAGE_POSITION_TOP_OR_LEFT + ? STAGE_POSITION_BOTTOM_OR_RIGHT : STAGE_POSITION_TOP_OR_LEFT; + } + + void setSideStagePosition(@SplitScreen.StagePosition int sideStagePosition) { + mSideStagePosition = sideStagePosition; + if (mSideStageListener.mVisible) { + onStageVisibilityChanged(mSideStageListener); + } + } + + void setSideStageVisibility(boolean visible) { + if (!mSideStageListener.mVisible == visible) return; + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mSideStage.setVisibility(visible, wct); + mTaskOrganizer.applyTransaction(wct); + } + + void exitSplitScreen() { + exitSplitScreen(null /* childrenToTop */); + } + + private void exitSplitScreen(StageTaskListener childrenToTop) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mSideStage.removeAllTasks(wct, childrenToTop == mSideStage); + mMainStage.deactivate(wct, childrenToTop == mMainStage); + mTaskOrganizer.applyTransaction(wct); + // Reset divider position. + mSplitLayout.resetDividerPosition(); + } + + void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) { + outTopOrLeftBounds.set(mSplitLayout.getBounds1()); + outBottomOrRightBounds.set(mSplitLayout.getBounds2()); + } + + void updateActivityOptions(Bundle opts, @SplitScreen.StagePosition int position) { + final StageTaskListener stage = position == mSideStagePosition ? mSideStage : mMainStage; + opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, stage.mRootTaskInfo.token); + + if (!mMainStage.isActive()) { + // Activate the main stage in anticipation of an app launch. + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mMainStage.activate(getMainStageBounds(), wct); + mSideStage.setBounds(getSideStageBounds(), wct); + mTaskOrganizer.applyTransaction(wct); + } + } + + void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) { + if (mListeners.contains(listener)) return; + mListeners.add(listener); + listener.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition()); + listener.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition()); + mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE); + mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN); + } + + void unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener) { + mListeners.remove(listener); + } + + private void onStageChildTaskStatusChanged( + StageListenerImpl stageListener, int taskId, boolean present) { + + int stage; + if (present) { + stage = stageListener == mSideStageListener ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN; + } else { + // No longer on any stage + stage = STAGE_TYPE_UNDEFINED; + } + + for (int i = mListeners.size() - 1; i >= 0; --i) { + mListeners.get(i).onTaskStageChanged(taskId, stage); + } + } + + private void onStageRootTaskAppeared(StageListenerImpl stageListener) { + if (mMainStageListener.mHasRootTask && mSideStageListener.mHasRootTask) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + // Make the stages adjacent to each other so they occlude what's behind them. + wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); + mTaskOrganizer.applyTransaction(wct); + } + } + + private void onStageRootTaskVanished(StageListenerImpl stageListener) { + if (stageListener == mMainStageListener || stageListener == mSideStageListener) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + // Deactivate the main stage if it no longer has a root task. + mMainStage.deactivate(wct); + mTaskOrganizer.applyTransaction(wct); + } + } + + private void onStageVisibilityChanged(StageListenerImpl stageListener) { + final boolean sideStageVisible = mSideStageListener.mVisible; + final boolean mainStageVisible = mMainStageListener.mVisible; + // Divider is only visible if both the main stage and side stages are visible + final boolean dividerVisible = sideStageVisible && mainStageVisible; + + if (mDividerVisible != dividerVisible) { + mDividerVisible = dividerVisible; + if (mDividerVisible) { + mSplitLayout.init(); + } else { + mSplitLayout.release(); + } + } + + if (!mainStageVisible && !sideStageVisible) { + // Exit split-screen if both stage are not visible. + // TODO: This is only a temporary request from UX and is likely to be removed soon... + exitSplitScreen(); + } + + if (mainStageVisible) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + if (sideStageVisible) { + // The main stage configuration should to follow split layout when side stage is + // visible. + mMainStage.updateConfiguration( + WINDOWING_MODE_MULTI_WINDOW, getMainStageBounds(), wct); + } else { + // We want the main stage configuration to be fullscreen when the side stage isn't + // visible. + mMainStage.updateConfiguration(WINDOWING_MODE_FULLSCREEN, null, wct); + } + // TODO: Change to `mSyncQueue.queue(wct)` once BLAST is stable. + mTaskOrganizer.applyTransaction(wct); + } + + mSyncQueue.runInSync(t -> { + final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); + final SurfaceControl sideStageLeash = mSideStage.mRootLeash; + final SurfaceControl mainStageLeash = mMainStage.mRootLeash; + + if (dividerLeash != null) { + if (mDividerVisible) { + t.show(dividerLeash) + .setLayer(dividerLeash, Integer.MAX_VALUE) + .setPosition(dividerLeash, + mSplitLayout.getDividerBounds().left, + mSplitLayout.getDividerBounds().top); + } else { + t.hide(dividerLeash); + } + } + + if (sideStageVisible) { + final Rect sideStageBounds = getSideStageBounds(); + t.show(sideStageLeash) + .setPosition(sideStageLeash, + sideStageBounds.left, sideStageBounds.top) + .setWindowCrop(sideStageLeash, + sideStageBounds.width(), sideStageBounds.height()); + } else { + t.hide(sideStageLeash); + } + + if (mainStageVisible) { + final Rect mainStageBounds = getMainStageBounds(); + t.show(mainStageLeash); + if (sideStageVisible) { + t.setPosition(mainStageLeash, mainStageBounds.left, mainStageBounds.top) + .setWindowCrop(mainStageLeash, + mainStageBounds.width(), mainStageBounds.height()); + } else { + // Clear window crop and position if side stage isn't visible. + t.setPosition(mainStageLeash, 0, 0) + .setWindowCrop(mainStageLeash, null); + } + } else { + t.hide(mainStageLeash); + } + }); + } + + private void onStageHasChildrenChanged(StageListenerImpl stageListener) { + final boolean hasChildren = stageListener.mHasChildren; + final boolean isSideStage = stageListener == mSideStageListener; + if (!hasChildren) { + if (isSideStage && mMainStageListener.mVisible) { + // Exit to main stage if side stage no longer has children. + exitSplitScreen(mMainStage); + } else if (!isSideStage && mSideStageListener.mVisible) { + // Exit to side stage if main stage no longer has children. + exitSplitScreen(mSideStage); + } + } else if (isSideStage) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + // Make sure the main stage is active. + mMainStage.activate(getMainStageBounds(), wct); + mTaskOrganizer.applyTransaction(wct); + } + } + + @Override + public void onSnappedToDismiss(boolean bottomOrRight) { + final boolean mainStageToTop = bottomOrRight + && mSideStagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT; + exitSplitScreen(mainStageToTop ? mMainStage : mSideStage); + } + + @Override + public void onBoundsChanging(SplitLayout layout) { + final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); + if (dividerLeash == null) return; + final Rect mainStageBounds = getMainStageBounds(); + final Rect sideStageBounds = getSideStageBounds(); + + mSyncQueue.runInSync(t -> t + .setPosition(dividerLeash, + mSplitLayout.getDividerBounds().left, mSplitLayout.getDividerBounds().top) + .setPosition(mMainStage.mRootLeash, mainStageBounds.left, mainStageBounds.top) + .setPosition(mSideStage.mRootLeash, sideStageBounds.left, sideStageBounds.top) + // Sets crop to prevent visible region of tasks overlap with each other when + // re-positioning surfaces while resizing. + .setWindowCrop(mMainStage.mRootLeash, + mainStageBounds.width(), mainStageBounds.height()) + .setWindowCrop(mSideStage.mRootLeash, + sideStageBounds.width(), sideStageBounds.height())); + + } + + @Override + public void onDoubleTappedDivider() { + setSideStagePosition(mSideStagePosition == STAGE_POSITION_TOP_OR_LEFT + ? STAGE_POSITION_BOTTOM_OR_RIGHT : STAGE_POSITION_TOP_OR_LEFT); + } + + @Override + public void onBoundsChanged(SplitLayout layout) { + final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); + if (dividerLeash == null) return; + final Rect mainStageBounds = getMainStageBounds(); + final Rect sideStageBounds = getSideStageBounds(); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mMainStage.setBounds(mainStageBounds, wct); + mSideStage.setBounds(sideStageBounds, wct); + mTaskOrganizer.applyTransaction(wct); + + mSyncQueue.runInSync(t -> t + // Resets layer of divider bar to make sure it is always on top. + .setLayer(dividerLeash, Integer.MAX_VALUE) + .setPosition(dividerLeash, + mSplitLayout.getDividerBounds().left, mSplitLayout.getDividerBounds().top) + .setPosition(mMainStage.mRootLeash, + mainStageBounds.left, mainStageBounds.top) + .setPosition(mSideStage.mRootLeash, + sideStageBounds.left, sideStageBounds.top) + // Resets crop to apply new surface bounds directly. + .setWindowCrop(mMainStage.mRootLeash, null) + .setWindowCrop(mSideStage.mRootLeash, null)); + } + + @Override + public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo) { + mDisplayAreaInfo = displayAreaInfo; + if (mSplitLayout == null) { + mSplitLayout = new SplitLayout(TAG + "SplitDivider", mContext, + mDisplayAreaInfo.configuration, this, + b -> mRootTDAOrganizer.attachToDisplayArea(mDisplayId, b)); + } + } + + @Override + public void onDisplayAreaVanished(DisplayAreaInfo displayAreaInfo) { + throw new IllegalStateException("Well that was unexpected..."); + } + + @Override + public void onDisplayAreaInfoChanged(DisplayAreaInfo displayAreaInfo) { + mDisplayAreaInfo = displayAreaInfo; + if (mSplitLayout != null + && mSplitLayout.updateConfiguration(mDisplayAreaInfo.configuration)) { + onBoundsChanged(mSplitLayout); + } + } + + private Rect getSideStageBounds() { + return mSideStagePosition == STAGE_POSITION_TOP_OR_LEFT + ? mSplitLayout.getBounds1() : mSplitLayout.getBounds2(); + } + + private Rect getMainStageBounds() { + return mSideStagePosition == STAGE_POSITION_TOP_OR_LEFT + ? mSplitLayout.getBounds2() : mSplitLayout.getBounds1(); + } + + @Override + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + final String childPrefix = innerPrefix + " "; + pw.println(prefix + TAG + " mDisplayId=" + mDisplayId); + pw.println(innerPrefix + "mDividerVisible=" + mDividerVisible); + pw.println(innerPrefix + "MainStage"); + pw.println(childPrefix + "isActive=" + mMainStage.isActive()); + mMainStageListener.dump(pw, childPrefix); + pw.println(innerPrefix + "SideStage"); + mSideStageListener.dump(pw, childPrefix); + pw.println(innerPrefix + "mSplitLayout=" + mSplitLayout); + } + + class StageListenerImpl implements StageTaskListener.StageListenerCallbacks { + boolean mHasRootTask = false; + boolean mVisible = false; + boolean mHasChildren = false; + + @Override + public void onRootTaskAppeared() { + mHasRootTask = true; + StageCoordinator.this.onStageRootTaskAppeared(this); + } + + @Override + public void onStatusChanged(boolean visible, boolean hasChildren) { + if (!mHasRootTask) return; + + if (mHasChildren != hasChildren) { + mHasChildren = hasChildren; + StageCoordinator.this.onStageHasChildrenChanged(this); + } + if (mVisible != visible) { + mVisible = visible; + StageCoordinator.this.onStageVisibilityChanged(this); + } + } + + @Override + public void onChildTaskStatusChanged(int taskId, boolean present) { + StageCoordinator.this.onStageChildTaskStatusChanged(this, taskId, present); + } + + @Override + public void onRootTaskVanished() { + reset(); + StageCoordinator.this.onStageRootTaskVanished(this); + } + + private void reset() { + mHasRootTask = false; + mVisible = false; + mHasChildren = false; + } + + public void dump(@NonNull PrintWriter pw, String prefix) { + pw.println(prefix + "mHasRootTask=" + mHasRootTask); + pw.println(prefix + "mVisible=" + mVisible); + pw.println(prefix + "mHasChildren=" + mHasChildren); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java new file mode 100644 index 000000000000..653299326cd0 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.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.splitscreen; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import android.annotation.CallSuper; +import android.app.ActivityManager; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.SparseArray; +import android.view.SurfaceControl; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.transition.Transitions; + +import java.io.PrintWriter; + +/** + * Base class that handle common task org. related for split-screen stages. + * Note that this class and its sub-class do not directly perform hierarchy operations. + * They only serve to hold a collection of tasks and provide APIs like + * {@link #setBounds(Rect, WindowContainerTransaction)} for the centralized {@link StageCoordinator} + * to perform operations in-sync with other containers. + * @see StageCoordinator + */ +class StageTaskListener implements ShellTaskOrganizer.TaskListener { + private static final String TAG = StageTaskListener.class.getSimpleName(); + + protected static final int[] CONTROLLED_ACTIVITY_TYPES = {ACTIVITY_TYPE_STANDARD}; + protected static final int[] CONTROLLED_WINDOWING_MODES = + {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED}; + protected static final int[] CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE = + {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW}; + + /** Callback interface for listening to changes in a split-screen stage. */ + public interface StageListenerCallbacks { + void onRootTaskAppeared(); + void onStatusChanged(boolean visible, boolean hasChildren); + void onChildTaskStatusChanged(int taskId, boolean present); + void onRootTaskVanished(); + } + private final StageListenerCallbacks mCallbacks; + private final SyncTransactionQueue mSyncQueue; + + protected ActivityManager.RunningTaskInfo mRootTaskInfo; + protected SurfaceControl mRootLeash; + protected SparseArray<ActivityManager.RunningTaskInfo> mChildrenTaskInfo = new SparseArray<>(); + private final SparseArray<SurfaceControl> mChildrenLeashes = new SparseArray<>(); + + StageTaskListener(ShellTaskOrganizer taskOrganizer, int displayId, + StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue) { + mCallbacks = callbacks; + mSyncQueue = syncQueue; + taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this); + } + + @Override + @CallSuper + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + if (mRootTaskInfo == null && !taskInfo.hasParentTask()) { + mRootLeash = leash; + mRootTaskInfo = taskInfo; + mCallbacks.onRootTaskAppeared(); + } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) { + final int taskId = taskInfo.taskId; + mChildrenLeashes.put(taskId, leash); + mChildrenTaskInfo.put(taskId, taskInfo); + updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */); + mCallbacks.onChildTaskStatusChanged(taskId, true /* present */); + } else { + throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo + + "\n mRootTaskInfo: " + mRootTaskInfo); + } + sendStatusChanged(); + } + + @Override + @CallSuper + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (mRootTaskInfo.taskId == taskInfo.taskId) { + mRootTaskInfo = taskInfo; + } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) { + mChildrenTaskInfo.put(taskInfo.taskId, taskInfo); + updateChildTaskSurface( + taskInfo, mChildrenLeashes.get(taskInfo.taskId), false /* firstAppeared */); + } else { + throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo + + "\n mRootTaskInfo: " + mRootTaskInfo); + } + sendStatusChanged(); + } + + @Override + @CallSuper + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + final int taskId = taskInfo.taskId; + if (mRootTaskInfo.taskId == taskId) { + mCallbacks.onRootTaskVanished(); + mRootTaskInfo = null; + } else if (mChildrenTaskInfo.contains(taskId)) { + mChildrenTaskInfo.remove(taskId); + mChildrenLeashes.remove(taskId); + sendStatusChanged(); + mCallbacks.onChildTaskStatusChanged(taskId, false /* present */); + } else { + throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo + + "\n mRootTaskInfo: " + mRootTaskInfo); + } + } + + void setBounds(Rect bounds, WindowContainerTransaction wct) { + wct.setBounds(mRootTaskInfo.token, bounds); + } + + void setVisibility(boolean visible, WindowContainerTransaction wct) { + wct.reorder(mRootTaskInfo.token, visible /* onTop */); + } + + void onSplitScreenListenerRegistered(SplitScreen.SplitScreenListener listener, + @SplitScreen.StageType int stage) { + for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { + listener.onTaskStageChanged(mChildrenTaskInfo.keyAt(i), stage); + } + } + + private void updateChildTaskSurface(ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl leash, boolean firstAppeared) { + final Point taskPositionInParent = taskInfo.positionInParent; + mSyncQueue.runInSync(t -> { + t.setWindowCrop(leash, null); + t.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y); + if (firstAppeared && !Transitions.ENABLE_SHELL_TRANSITIONS) { + t.setAlpha(leash, 1f); + t.setMatrix(leash, 1, 0, 0, 1); + t.show(leash); + } + }); + } + + private void sendStatusChanged() { + mCallbacks.onStatusChanged(mRootTaskInfo.isVisible, mChildrenTaskInfo.size() > 0); + } + + @Override + @CallSuper + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + final String childPrefix = innerPrefix + " "; + pw.println(prefix + this); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java new file mode 100644 index 000000000000..f3f2fc3686b6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java @@ -0,0 +1,565 @@ +/* + * 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.startingsurface; + +import android.annotation.NonNull; +import android.app.ActivityThread; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.os.Build; +import android.util.Slog; +import android.view.Gravity; +import android.view.View; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import com.android.internal.R; +import com.android.internal.graphics.palette.Palette; +import com.android.internal.graphics.palette.Quantizer; +import com.android.internal.graphics.palette.VariationalKMeansQuantizer; +import com.android.internal.policy.PhoneWindow; + +import java.util.List; + +/** + * Util class to create the view for a splash screen content. + */ +class SplashscreenContentDrawer { + private static final String TAG = StartingSurfaceDrawer.TAG; + private static final boolean DEBUG = StartingSurfaceDrawer.DEBUG_SPLASH_SCREEN; + + // The acceptable area ratio of foreground_icon_area/background_icon_area, if there is an + // icon which it's non-transparent foreground area is similar to it's background area, then + // do not enlarge the foreground drawable. + // For example, an icon with the foreground 108*108 opaque pixels and it's background + // also 108*108 pixels, then do not enlarge this icon if only need to show foreground icon. + private static final float ENLARGE_FOREGROUND_ICON_THRESHOLD = (72f * 72f) / (108f * 108f); + private final Context mContext; + private int mIconSize; + + SplashscreenContentDrawer(Context context) { + mContext = context; + } + + private void updateDensity() { + mIconSize = mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.starting_surface_icon_size); + } + + private int getSystemBGColor() { + final Context systemContext = ActivityThread.currentApplication(); + if (systemContext == null) { + Slog.e(TAG, "System context does not exist!"); + return Color.BLACK; + } + final Resources res = systemContext.getResources(); + return res.getColor(com.android.wm.shell.R.color.splash_window_background_default); + } + + private Drawable createDefaultBackgroundDrawable() { + return new ColorDrawable(getSystemBGColor()); + } + + View makeSplashScreenContentView(PhoneWindow win, Context context, int iconRes, + int splashscreenContentResId) { + updateDensity(); + win.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + // splash screen content will be deprecated after S. + final View ssc = makeSplashscreenContentDrawable(win, context, splashscreenContentResId); + if (ssc != null) { + return ssc; + } + + final TypedArray typedArray = context.obtainStyledAttributes( + com.android.internal.R.styleable.Window); + final int resId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0); + typedArray.recycle(); + final Drawable themeBGDrawable; + if (resId == 0) { + Slog.w(TAG, "Window background not exist!"); + themeBGDrawable = createDefaultBackgroundDrawable(); + } else { + themeBGDrawable = context.getDrawable(resId); + } + final Drawable iconDrawable = iconRes != 0 ? context.getDrawable(iconRes) + : context.getPackageManager().getDefaultActivityIcon(); + // TODO (b/173975965) Tracking the performance on improved splash screen. + final StartingWindowViewBuilder builder = new StartingWindowViewBuilder(); + return builder + .setPhoneWindow(win) + .setContext(context) + .setThemeDrawable(themeBGDrawable) + .setIconDrawable(iconDrawable).build(); + } + + private class StartingWindowViewBuilder { + // materials + private Drawable mThemeBGDrawable; + private Drawable mIconDrawable; + private PhoneWindow mPhoneWindow; + private Context mContext; + + // result + private boolean mBuildComplete = false; + private View mCachedResult; + private int mThemeColor; + private Drawable mFinalIconDrawable; + private float mScale = 1f; + + StartingWindowViewBuilder setThemeDrawable(Drawable background) { + mThemeBGDrawable = background; + mBuildComplete = false; + return this; + } + + StartingWindowViewBuilder setIconDrawable(Drawable iconDrawable) { + mIconDrawable = iconDrawable; + mBuildComplete = false; + return this; + } + + StartingWindowViewBuilder setPhoneWindow(PhoneWindow window) { + mPhoneWindow = window; + mBuildComplete = false; + return this; + } + + StartingWindowViewBuilder setContext(Context context) { + mContext = context; + mBuildComplete = false; + return this; + } + + View build() { + if (mBuildComplete) { + return mCachedResult; + } + if (mPhoneWindow == null || mContext == null) { + Slog.e(TAG, "Unable to create StartingWindowView, lack of materials!"); + return null; + } + if (mThemeBGDrawable == null) { + Slog.w(TAG, "Theme Background Drawable is null, forget to set Theme Drawable?"); + mThemeBGDrawable = createDefaultBackgroundDrawable(); + } + processThemeColor(); + if (!processAdaptiveIcon() && mIconDrawable != null) { + if (DEBUG) { + Slog.d(TAG, "The icon is not an AdaptiveIconDrawable"); + } + mFinalIconDrawable = mIconDrawable; + } + final int iconSize = mFinalIconDrawable != null ? (int) (mIconSize * mScale) : 0; + mCachedResult = fillViewWithIcon(mPhoneWindow, mContext, iconSize, mFinalIconDrawable); + mBuildComplete = true; + return mCachedResult; + } + + private void processThemeColor() { + final DrawableColorTester themeBGTester = + new DrawableColorTester(mThemeBGDrawable, true /* filterTransparent */); + if (themeBGTester.nonTransparentRatio() == 0) { + // the window background is transparent, unable to draw + Slog.w(TAG, "Window background is transparent, fill background with black color"); + mThemeColor = getSystemBGColor(); + } else { + mThemeColor = themeBGTester.getDominateColor(); + } + } + + private boolean processAdaptiveIcon() { + if (!(mIconDrawable instanceof AdaptiveIconDrawable)) { + return false; + } + + final AdaptiveIconDrawable adaptiveIconDrawable = (AdaptiveIconDrawable) mIconDrawable; + final DrawableColorTester backIconTester = + new DrawableColorTester(adaptiveIconDrawable.getBackground()); + + final Drawable iconForeground = adaptiveIconDrawable.getForeground(); + final DrawableColorTester foreIconTester = + new DrawableColorTester(iconForeground, true /* filterTransparent */); + + final boolean foreComplex = foreIconTester.isComplexColor(); + final int foreMainColor = foreIconTester.getDominateColor(); + + if (DEBUG) { + Slog.d(TAG, "foreground complex color? " + foreComplex + " main color: " + + Integer.toHexString(foreMainColor)); + } + final boolean backComplex = backIconTester.isComplexColor(); + final int backMainColor = backIconTester.getDominateColor(); + if (DEBUG) { + Slog.d(TAG, "background complex color? " + backComplex + " main color: " + + Integer.toHexString(backMainColor)); + Slog.d(TAG, "theme color? " + Integer.toHexString(mThemeColor)); + } + + // Only draw the foreground of AdaptiveIcon to the splash screen if below condition + // meet: + // A. The background of the adaptive icon is not complicated. If it is complicated, + // it may contain some information, and + // B. The background of the adaptive icon is similar to the theme color, or + // C. The background of the adaptive icon is grayscale, and the foreground of the + // adaptive icon forms a certain contrast with the theme color. + if (!backComplex && (isRgbSimilarInHsv(mThemeColor, backMainColor) + || (backIconTester.isGrayscale() + && !isRgbSimilarInHsv(mThemeColor, foreMainColor)))) { + if (DEBUG) { + Slog.d(TAG, "makeSplashScreenContentView: choose fg icon"); + } + // Using AdaptiveIconDrawable here can help keep the shape consistent with the + // current settings. + mFinalIconDrawable = new AdaptiveIconDrawable( + new ColorDrawable(mThemeColor), iconForeground); + // Reference AdaptiveIcon description, outer is 108 and inner is 72, so we + // should enlarge the size 108/72 if we only draw adaptiveIcon's foreground. + if (foreIconTester.nonTransparentRatio() < ENLARGE_FOREGROUND_ICON_THRESHOLD) { + mScale = 1.5f; + } + } else { + if (DEBUG) { + Slog.d(TAG, "makeSplashScreenContentView: draw whole icon"); + } + mFinalIconDrawable = adaptiveIconDrawable; + } + return true; + } + + private View fillViewWithIcon(PhoneWindow win, Context context, + int iconSize, Drawable iconDrawable) { + final StartingSurfaceWindowView surfaceWindowView = + new StartingSurfaceWindowView(context, iconSize); + surfaceWindowView.setBackground(new ColorDrawable(mThemeColor)); + if (iconDrawable != null) { + surfaceWindowView.setIconDrawable(iconDrawable); + } + if (DEBUG) { + Slog.d(TAG, "fillViewWithIcon surfaceWindowView " + surfaceWindowView); + } + win.setContentView(surfaceWindowView); + makeSystemUIColorsTransparent(win); + return surfaceWindowView; + } + + private void makeSystemUIColorsTransparent(PhoneWindow win) { + win.setStatusBarColor(Color.TRANSPARENT); + win.setNavigationBarColor(Color.TRANSPARENT); + } + } + + private static boolean isRgbSimilarInHsv(int a, int b) { + if (a == b) { + return true; + } + final float[] aHsv = new float[3]; + final float[] bHsv = new float[3]; + Color.colorToHSV(a, aHsv); + Color.colorToHSV(b, bHsv); + // Minimum degree of the hue between two colors, the result range is 0-180. + int minAngle = (int) Math.abs(aHsv[0] - bHsv[0]); + minAngle = (minAngle + 180) % 360 - 180; + + // Calculate the difference between two colors based on the HSV dimensions. + final float normalizeH = minAngle / 180f; + final double square = Math.pow(normalizeH, 2) + + Math.pow(aHsv[1] - bHsv[1], 2) + + Math.pow(aHsv[2] - bHsv[2], 2); + final double mean = square / 3; + final double root = Math.sqrt(mean); + if (DEBUG) { + Slog.d(TAG, "hsvDiff " + minAngle + " a: " + Integer.toHexString(a) + + " b " + Integer.toHexString(b) + " ah " + aHsv[0] + " bh " + bHsv[0] + + " root " + root); + } + return root < 0.1; + } + + private static View makeSplashscreenContentDrawable(PhoneWindow win, Context ctx, + int splashscreenContentResId) { + // doesn't support windowSplashscreenContent after S + // TODO add an allowlist to skip some packages if needed + final int targetSdkVersion = ctx.getApplicationInfo().targetSdkVersion; + if (DEBUG) { + Slog.d(TAG, "target sdk for package: " + targetSdkVersion); + } + if (targetSdkVersion >= Build.VERSION_CODES.S) { + return null; + } + if (splashscreenContentResId == 0) { + return null; + } + final Drawable drawable = ctx.getDrawable(splashscreenContentResId); + if (drawable == null) { + return null; + } + View view = new View(ctx); + view.setBackground(drawable); + win.setContentView(view); + return view; + } + + private static class DrawableColorTester { + private final ColorTester mColorChecker; + + DrawableColorTester(Drawable drawable) { + this(drawable, false /* filterTransparent */); + } + + DrawableColorTester(Drawable drawable, boolean filterTransparent) { + // Some applications use LayerDrawable for their windowBackground. To ensure that we + // only get the real background, so that the color is not affected by the alpha of the + // upper layer, try to get the lower layer here. This can also speed up the calculation. + if (drawable instanceof LayerDrawable) { + LayerDrawable layerDrawable = (LayerDrawable) drawable; + if (layerDrawable.getNumberOfLayers() > 0) { + if (DEBUG) { + Slog.d(TAG, "replace drawable with bottom layer drawable"); + } + drawable = layerDrawable.getDrawable(0); + } + } + mColorChecker = drawable instanceof ColorDrawable + ? new SingleColorTester((ColorDrawable) drawable) + : new ComplexDrawableTester(drawable, filterTransparent); + } + + public float nonTransparentRatio() { + return mColorChecker.nonTransparentRatio(); + } + + public boolean isComplexColor() { + return mColorChecker.isComplexColor(); + } + + public int getDominateColor() { + return mColorChecker.getDominantColor(); + } + + public boolean isGrayscale() { + return mColorChecker.isGrayscale(); + } + + /** + * A help class to check the color information from a Drawable. + */ + private interface ColorTester { + float nonTransparentRatio(); + boolean isComplexColor(); + int getDominantColor(); + boolean isGrayscale(); + } + + private static boolean isGrayscaleColor(int color) { + final int red = Color.red(color); + final int green = Color.green(color); + final int blue = Color.blue(color); + return red == green && green == blue; + } + + /** + * For ColorDrawable only. + * There will be only one color so don't spend too much resource for it. + */ + private static class SingleColorTester implements ColorTester { + private final ColorDrawable mColorDrawable; + + SingleColorTester(@NonNull ColorDrawable drawable) { + mColorDrawable = drawable; + } + + @Override + public float nonTransparentRatio() { + final int alpha = mColorDrawable.getAlpha(); + return (float) (alpha / 255); + } + + @Override + public boolean isComplexColor() { + return false; + } + + @Override + public int getDominantColor() { + return mColorDrawable.getColor(); + } + + @Override + public boolean isGrayscale() { + return isGrayscaleColor(mColorDrawable.getColor()); + } + } + + /** + * For any other Drawable except ColorDrawable. + * This will use the Palette API to check the color information and use a quantizer to + * filter out transparent colors when needed. + */ + private static class ComplexDrawableTester implements ColorTester { + private static final int MAX_BITMAP_SIZE = 40; + private final Palette mPalette; + private final boolean mFilterTransparent; + private static final TransparentFilterQuantizer TRANSPARENT_FILTER_QUANTIZER = + new TransparentFilterQuantizer(); + + ComplexDrawableTester(Drawable drawable, boolean filterTransparent) { + final Rect initialBounds = drawable.copyBounds(); + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + + // Some drawables do not have intrinsic dimensions + if (width <= 0 || height <= 0) { + width = MAX_BITMAP_SIZE; + height = MAX_BITMAP_SIZE; + } else { + width = Math.min(width, MAX_BITMAP_SIZE); + height = Math.min(height, MAX_BITMAP_SIZE); + } + + final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + final Canvas bmpCanvas = new Canvas(bitmap); + drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight()); + drawable.draw(bmpCanvas); + // restore to original bounds + drawable.setBounds(initialBounds); + + final Palette.Builder builder = new Palette.Builder(bitmap) + .maximumColorCount(5).clearFilters(); + // The Palette API will ignore Alpha, so it cannot handle transparent pixels, but + // sometimes we will need this information to know if this Drawable object is + // transparent. + mFilterTransparent = filterTransparent; + if (mFilterTransparent) { + builder.setQuantizer(TRANSPARENT_FILTER_QUANTIZER); + } + mPalette = builder.generate(); + bitmap.recycle(); + } + + @Override + public float nonTransparentRatio() { + return mFilterTransparent ? TRANSPARENT_FILTER_QUANTIZER.mNonTransparentRatio : 1; + } + + @Override + public boolean isComplexColor() { + return mPalette.getSwatches().size() > 1; + } + + @Override + public int getDominantColor() { + final Palette.Swatch mainSwatch = mPalette.getDominantSwatch(); + if (mainSwatch != null) { + return mainSwatch.getRgb(); + } + return Color.BLACK; + } + + @Override + public boolean isGrayscale() { + final List<Palette.Swatch> swatches = mPalette.getSwatches(); + if (swatches != null) { + for (int i = swatches.size() - 1; i >= 0; i--) { + Palette.Swatch swatch = swatches.get(i); + if (!isGrayscaleColor(swatch.getRgb())) { + return false; + } + } + } + return true; + } + + private static class TransparentFilterQuantizer implements Quantizer { + private static final int NON_TRANSPARENT = 0xFF000000; + private final Quantizer mInnerQuantizer = new VariationalKMeansQuantizer(); + private float mNonTransparentRatio; + @Override + public void quantize(final int[] pixels, final int maxColors, + final Palette.Filter[] filters) { + mNonTransparentRatio = 0; + int realSize = 0; + for (int i = pixels.length - 1; i > 0; i--) { + if ((pixels[i] & NON_TRANSPARENT) != 0) { + realSize++; + } + } + if (realSize == 0) { + if (DEBUG) { + Slog.d(TAG, "quantize: this is pure transparent image"); + } + mInnerQuantizer.quantize(pixels, maxColors, filters); + return; + } + mNonTransparentRatio = (float) realSize / pixels.length; + final int[] samplePixels = new int[realSize]; + int rowIndex = 0; + for (int i = pixels.length - 1; i > 0; i--) { + if ((pixels[i] & NON_TRANSPARENT) == NON_TRANSPARENT) { + samplePixels[rowIndex] = pixels[i]; + rowIndex++; + } + } + mInnerQuantizer.quantize(samplePixels, maxColors, filters); + } + + @Override + public List<Palette.Swatch> getQuantizedColors() { + return mInnerQuantizer.getQuantizedColors(); + } + } + } + } + + private static class StartingSurfaceWindowView extends FrameLayout { + // TODO animate the icon view + private final View mIconView; + + StartingSurfaceWindowView(Context context, int iconSize) { + super(context); + + final boolean emptyIcon = iconSize == 0; + if (emptyIcon) { + mIconView = null; + } else { + mIconView = new View(context); + FrameLayout.LayoutParams params = + new FrameLayout.LayoutParams(iconSize, iconSize); + params.gravity = Gravity.CENTER; + addView(mIconView, params); + } + setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } + + // TODO support animatable icon + void setIconDrawable(Drawable icon) { + if (mIconView != null) { + mIconView.setBackground(icon); + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java new file mode 100644 index 000000000000..8e24e0b516cb --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java @@ -0,0 +1,472 @@ +/* + * 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.startingsurface; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.content.Context.CONTEXT_RESTRICTED; +import static android.content.res.Configuration.EMPTY; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.window.StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_CREATED; +import static android.window.StartingWindowInfo.TYPE_PARAMETER_ALLOW_TASK_SNAPSHOT; +import static android.window.StartingWindowInfo.TYPE_PARAMETER_NEW_TASK; +import static android.window.StartingWindowInfo.TYPE_PARAMETER_PROCESS_RUNNING; +import static android.window.StartingWindowInfo.TYPE_PARAMETER_TASK_SWITCH; + +import android.app.ActivityManager.RunningTaskInfo; +import android.app.ActivityTaskManager; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.hardware.display.DisplayManager; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Slog; +import android.util.SparseArray; +import android.view.Display; +import android.view.View; +import android.view.WindowManager; +import android.window.StartingWindowInfo; +import android.window.TaskOrganizer; +import android.window.TaskSnapshot; + +import com.android.internal.R; +import com.android.internal.policy.PhoneWindow; +import com.android.wm.shell.common.ShellExecutor; + +import java.util.function.Consumer; + +/** + * Implementation to draw the starting window to an application, and remove the starting window + * until the application displays its own window. + * + * When receive {@link TaskOrganizer#addStartingWindow} callback, use this class to create a + * starting window and attached to the Task, then when the Task want to remove the starting window, + * the TaskOrganizer will receive {@link TaskOrganizer#removeStartingWindow} callback then use this + * class to remove the starting window of the Task. + * @hide + */ + +public class StartingSurfaceDrawer { + static final String TAG = StartingSurfaceDrawer.class.getSimpleName(); + static final boolean DEBUG_SPLASH_SCREEN = false; + static final boolean DEBUG_TASK_SNAPSHOT = false; + + private final Context mContext; + private final DisplayManager mDisplayManager; + final ShellExecutor mMainExecutor; + private final SplashscreenContentDrawer mSplashscreenContentDrawer; + + // TODO(b/131727939) remove this when clearing ActivityRecord + private static final int REMOVE_WHEN_TIMEOUT = 2000; + + public StartingSurfaceDrawer(Context context, ShellExecutor mainExecutor) { + mContext = context; + mDisplayManager = mContext.getSystemService(DisplayManager.class); + mMainExecutor = mainExecutor; + mSplashscreenContentDrawer = new SplashscreenContentDrawer(context); + } + + private final SparseArray<StartingWindowRecord> mStartingWindowRecords = new SparseArray<>(); + + /** Obtain proper context for showing splash screen on the provided display. */ + private Context getDisplayContext(Context context, int displayId) { + if (displayId == DEFAULT_DISPLAY) { + // The default context fits. + return context; + } + + final Display targetDisplay = mDisplayManager.getDisplay(displayId); + if (targetDisplay == null) { + // Failed to obtain the non-default display where splash screen should be shown, + // lets not show at all. + return null; + } + + return context.createDisplayContext(targetDisplay); + } + + private static class PreferredStartingTypeHelper { + private static final int STARTING_TYPE_NO = 0; + private static final int STARTING_TYPE_SPLASH_SCREEN = 1; + private static final int STARTING_TYPE_SNAPSHOT = 2; + + TaskSnapshot mSnapshot; + int mPreferredType; + + PreferredStartingTypeHelper(StartingWindowInfo taskInfo) { + final int parameter = taskInfo.startingWindowTypeParameter; + final boolean newTask = (parameter & TYPE_PARAMETER_NEW_TASK) != 0; + final boolean taskSwitch = (parameter & TYPE_PARAMETER_TASK_SWITCH) != 0; + final boolean processRunning = (parameter & TYPE_PARAMETER_PROCESS_RUNNING) != 0; + final boolean allowTaskSnapshot = (parameter & TYPE_PARAMETER_ALLOW_TASK_SNAPSHOT) != 0; + final boolean activityCreated = (parameter & TYPE_PARAMETER_ACTIVITY_CREATED) != 0; + mPreferredType = preferredStartingWindowType(taskInfo, newTask, taskSwitch, + processRunning, allowTaskSnapshot, activityCreated); + } + + // reference from ActivityRecord#getStartingWindowType + private int preferredStartingWindowType(StartingWindowInfo windowInfo, + boolean newTask, boolean taskSwitch, boolean processRunning, + boolean allowTaskSnapshot, boolean activityCreated) { + if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) { + Slog.d(TAG, "preferredStartingWindowType newTask " + newTask + + " taskSwitch " + taskSwitch + + " processRunning " + processRunning + + " allowTaskSnapshot " + allowTaskSnapshot + + " activityCreated " + activityCreated); + } + + if (newTask || !processRunning || (taskSwitch && !activityCreated)) { + return STARTING_TYPE_SPLASH_SCREEN; + } else if (taskSwitch && allowTaskSnapshot) { + final TaskSnapshot snapshot = getTaskSnapshot(windowInfo.taskInfo.taskId); + if (isSnapshotCompatible(windowInfo, snapshot)) { + return STARTING_TYPE_SNAPSHOT; + } + if (windowInfo.taskInfo.topActivityType != ACTIVITY_TYPE_HOME) { + return STARTING_TYPE_SPLASH_SCREEN; + } + return STARTING_TYPE_NO; + } else { + return STARTING_TYPE_NO; + } + } + + /** + * Returns {@code true} if the task snapshot is compatible with this activity (at least the + * rotation must be the same). + */ + private boolean isSnapshotCompatible(StartingWindowInfo windowInfo, TaskSnapshot snapshot) { + if (snapshot == null) { + if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) { + Slog.d(TAG, "isSnapshotCompatible no snapshot " + windowInfo.taskInfo.taskId); + } + return false; + } + + final int taskRotation = windowInfo.taskInfo.configuration + .windowConfiguration.getRotation(); + final int snapshotRotation = snapshot.getRotation(); + if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) { + Slog.d(TAG, "isSnapshotCompatible rotation " + taskRotation + + " snapshot " + snapshotRotation); + } + return taskRotation == snapshotRotation; + } + + private TaskSnapshot getTaskSnapshot(int taskId) { + if (mSnapshot != null) { + return mSnapshot; + } + try { + mSnapshot = ActivityTaskManager.getService().getTaskSnapshot(taskId, + false/* isLowResolution */); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to get snapshot for task: " + taskId + ", from: " + e); + return null; + } + return mSnapshot; + } + } + + /** + * Called when a task need a starting window. + */ + public void addStartingWindow(StartingWindowInfo windowInfo, IBinder appToken) { + final PreferredStartingTypeHelper helper = + new PreferredStartingTypeHelper(windowInfo); + final RunningTaskInfo runningTaskInfo = windowInfo.taskInfo; + if (helper.mPreferredType == PreferredStartingTypeHelper.STARTING_TYPE_SPLASH_SCREEN) { + addSplashScreenStartingWindow(runningTaskInfo, appToken); + } else if (helper.mPreferredType == PreferredStartingTypeHelper.STARTING_TYPE_SNAPSHOT) { + final TaskSnapshot snapshot = helper.mSnapshot; + makeTaskSnapshotWindow(windowInfo, appToken, snapshot); + } + // If prefer don't show, then don't show! + } + + private void addSplashScreenStartingWindow(RunningTaskInfo taskInfo, IBinder appToken) { + final ActivityInfo activityInfo = taskInfo.topActivityInfo; + if (activityInfo == null) { + return; + } + final int displayId = taskInfo.displayId; + if (activityInfo.packageName == null) { + return; + } + + CharSequence nonLocalizedLabel = activityInfo.nonLocalizedLabel; + int labelRes = activityInfo.labelRes; + if (activityInfo.nonLocalizedLabel == null && activityInfo.labelRes == 0) { + ApplicationInfo app = activityInfo.applicationInfo; + nonLocalizedLabel = app.nonLocalizedLabel; + labelRes = app.labelRes; + } + + Context context = mContext; + int theme = activityInfo.getThemeResource(); + if (theme == 0) { + // replace with the default theme if the application didn't set + theme = com.android.internal.R.style.Theme_DeviceDefault_DayNight; + } + if (DEBUG_SPLASH_SCREEN) { + Slog.d(TAG, "addSplashScreen " + activityInfo.packageName + + ": nonLocalizedLabel=" + nonLocalizedLabel + " theme=" + + Integer.toHexString(theme) + " task= " + taskInfo.taskId); + } + + // Obtain proper context to launch on the right display. + final Context displayContext = getDisplayContext(context, displayId); + if (displayContext == null) { + // Can't show splash screen on requested display, so skip showing at all. + return; + } + context = displayContext; + if (theme != context.getThemeResId() || labelRes != 0) { + try { + context = context.createPackageContext( + activityInfo.packageName, CONTEXT_RESTRICTED); + context.setTheme(theme); + } catch (PackageManager.NameNotFoundException e) { + // Ignore + } + } + + final Configuration taskConfig = taskInfo.getConfiguration(); + if (taskConfig != null && !taskConfig.equals(EMPTY)) { + if (DEBUG_SPLASH_SCREEN) { + Slog.d(TAG, "addSplashScreen: creating context based" + + " on task Configuration " + taskConfig + " for splash screen"); + } + final Context overrideContext = context.createConfigurationContext(taskConfig); + overrideContext.setTheme(theme); + final TypedArray typedArray = overrideContext.obtainStyledAttributes( + com.android.internal.R.styleable.Window); + final int resId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0); + if (resId != 0 && overrideContext.getDrawable(resId) != null) { + // We want to use the windowBackground for the override context if it is + // available, otherwise we use the default one to make sure a themed starting + // window is displayed for the app. + if (DEBUG_SPLASH_SCREEN) { + Slog.d(TAG, "addSplashScreen: apply overrideConfig" + + taskConfig + " to starting window resId=" + resId); + } + context = overrideContext; + } + typedArray.recycle(); + } + + int windowFlags = 0; + if ((activityInfo.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0) { + windowFlags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; + } + + final boolean[] showWallpaper = new boolean[1]; + final int[] splashscreenContentResId = new int[1]; + getWindowResFromContext(context, a -> { + splashscreenContentResId[0] = + a.getResourceId(R.styleable.Window_windowSplashscreenContent, 0); + showWallpaper[0] = a.getBoolean(R.styleable.Window_windowShowWallpaper, false); + }); + if (showWallpaper[0]) { + windowFlags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; + } + + final PhoneWindow win = new PhoneWindow(context); + win.setIsStartingWindow(true); + + CharSequence label = context.getResources().getText(labelRes, null); + // Only change the accessibility title if the label is localized + if (label != null) { + win.setTitle(label, true); + } else { + win.setTitle(nonLocalizedLabel, false); + } + + win.setType(WindowManager.LayoutParams.TYPE_APPLICATION_STARTING); + + // Assumes it's safe to show starting windows of launched apps while + // the keyguard is being hidden. This is okay because starting windows never show + // secret information. + // TODO(b/113840485): Occluded may not only happen on default display + if (displayId == DEFAULT_DISPLAY) { + windowFlags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; + } + + // Force the window flags: this is a fake window, so it is not really + // touchable or focusable by the user. We also add in the ALT_FOCUSABLE_IM + // flag because we do know that the next window will take input + // focus, so we want to get the IME window up on top of us right away. + win.setFlags(windowFlags + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, + windowFlags + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); + + final int iconRes = activityInfo.getIconResource(); + final int logoRes = activityInfo.getLogoResource(); + win.setDefaultIcon(iconRes); + win.setDefaultLogo(logoRes); + + win.setLayout(WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.MATCH_PARENT); + + final WindowManager.LayoutParams params = win.getAttributes(); + params.token = appToken; + params.packageName = activityInfo.packageName; + params.windowAnimations = win.getWindowStyle().getResourceId( + com.android.internal.R.styleable.Window_windowAnimationStyle, 0); + params.privateFlags |= + WindowManager.LayoutParams.PRIVATE_FLAG_FAKE_HARDWARE_ACCELERATED; + params.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + // Setting as trusted overlay to let touches pass through. This is safe because this + // window is controlled by the system. + params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; + + final Resources res = context.getResources(); + final boolean supportsScreen = res != null && (res.getCompatibilityInfo() != null + && res.getCompatibilityInfo().supportsScreen()); + if (!supportsScreen) { + params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW; + } + + params.setTitle("Splash Screen " + activityInfo.packageName); + final View contentView = mSplashscreenContentDrawer.makeSplashScreenContentView(win, + context, iconRes, splashscreenContentResId[0]); + if (contentView == null) { + Slog.w(TAG, "Adding splash screen window for " + activityInfo.packageName + " failed!"); + return; + } + + final View view = win.getDecorView(); + + if (DEBUG_SPLASH_SCREEN) { + Slog.d(TAG, "Adding splash screen window for " + + activityInfo.packageName + " / " + appToken + ": " + view); + } + final WindowManager wm = context.getSystemService(WindowManager.class); + postAddWindow(taskInfo.taskId, appToken, view, wm, params); + } + + /** + * Called when a task need a snapshot starting window. + */ + private void makeTaskSnapshotWindow(StartingWindowInfo startingWindowInfo, + IBinder appToken, TaskSnapshot snapshot) { + final int taskId = startingWindowInfo.taskInfo.taskId; + final TaskSnapshotWindow surface = TaskSnapshotWindow.create(startingWindowInfo, appToken, + snapshot, mMainExecutor, () -> removeWindowSynced(taskId) /* clearWindow */); + mMainExecutor.execute(() -> { + mMainExecutor.executeDelayed(() -> removeWindowSynced(taskId), REMOVE_WHEN_TIMEOUT); + final StartingWindowRecord tView = + new StartingWindowRecord(null/* decorView */, surface); + mStartingWindowRecords.put(taskId, tView); + }); + } + + /** + * Called when the content of a task is ready to show, starting window can be removed. + */ + public void removeStartingWindow(int taskId) { + if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) { + Slog.d(TAG, "Task start finish, remove starting surface for task " + taskId); + } + mMainExecutor.execute(() -> removeWindowSynced(taskId)); + } + + protected void postAddWindow(int taskId, IBinder appToken, + View view, WindowManager wm, WindowManager.LayoutParams params) { + mMainExecutor.execute(() -> { + boolean shouldSaveView = true; + try { + wm.addView(view, params); + } catch (WindowManager.BadTokenException e) { + // ignore + Slog.w(TAG, appToken + " already running, starting window not displayed. " + + e.getMessage()); + shouldSaveView = false; + } catch (RuntimeException e) { + // don't crash if something else bad happens, for example a + // failure loading resources because we are loading from an app + // on external storage that has been unmounted. + Slog.w(TAG, appToken + " failed creating starting window", e); + shouldSaveView = false; + } finally { + if (view != null && view.getParent() == null) { + Slog.w(TAG, "view not successfully added to wm, removing view"); + wm.removeViewImmediate(view); + shouldSaveView = false; + } + } + + if (shouldSaveView) { + removeWindowSynced(taskId); + mMainExecutor.executeDelayed(() -> removeWindowSynced(taskId), REMOVE_WHEN_TIMEOUT); + final StartingWindowRecord tView = + new StartingWindowRecord(view, null /* TaskSnapshotWindow */); + mStartingWindowRecords.put(taskId, tView); + } + }); + } + + protected void removeWindowSynced(int taskId) { + final StartingWindowRecord record = mStartingWindowRecords.get(taskId); + if (record != null) { + if (record.mDecorView != null) { + if (DEBUG_SPLASH_SCREEN) { + Slog.v(TAG, "Removing splash screen window for task: " + taskId); + } + final WindowManager wm = record.mDecorView.getContext() + .getSystemService(WindowManager.class); + wm.removeView(record.mDecorView); + } + if (record.mTaskSnapshotWindow != null) { + if (DEBUG_TASK_SNAPSHOT) { + Slog.v(TAG, "Removing task snapshot window for " + taskId); + } + record.mTaskSnapshotWindow.remove(mMainExecutor); + } + mStartingWindowRecords.remove(taskId); + } + } + + private void getWindowResFromContext(Context ctx, Consumer<TypedArray> consumer) { + final TypedArray a = ctx.obtainStyledAttributes(R.styleable.Window); + consumer.accept(a); + a.recycle(); + } + + /** + * Record the view or surface for a starting window. + */ + private static class StartingWindowRecord { + private final View mDecorView; + private final TaskSnapshotWindow mTaskSnapshotWindow; + + StartingWindowRecord(View decorView, TaskSnapshotWindow taskSnapshotWindow) { + mDecorView = decorView; + mTaskSnapshotWindow = taskSnapshotWindow; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java new file mode 100644 index 000000000000..b5e18960ff5c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java @@ -0,0 +1,620 @@ +/* + * 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.startingsurface; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.graphics.Color.WHITE; +import static android.graphics.Color.alpha; +import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; +import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; +import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; +import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; +import static android.view.WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES; +import static android.view.WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE; +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_SCALED; +import static android.view.WindowManager.LayoutParams.FLAG_SECURE; +import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; +import static android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; +import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; +import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS; +import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; + +import static com.android.internal.policy.DecorView.NAVIGATION_BAR_COLOR_VIEW_ATTRIBUTES; +import static com.android.internal.policy.DecorView.STATUS_BAR_COLOR_VIEW_ATTRIBUTES; +import static com.android.internal.policy.DecorView.getNavigationBarRect; + +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.ActivityManager.TaskDescription; +import android.app.ActivityThread; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.hardware.HardwareBuffer; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.SystemClock; +import android.util.MergedConfiguration; +import android.util.Slog; +import android.view.IWindowSession; +import android.view.InputChannel; +import android.view.InsetsSourceControl; +import android.view.InsetsState; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.SurfaceSession; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowManagerGlobal; +import android.window.ClientWindowFrames; +import android.window.StartingWindowInfo; +import android.window.TaskSnapshot; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.policy.DecorView; +import com.android.internal.view.BaseIWindow; +import com.android.wm.shell.common.ShellExecutor; + +/** + * This class represents a starting window that shows a snapshot. + * + * @hide + */ +public class TaskSnapshotWindow { + + private static final long SIZE_MISMATCH_MINIMUM_TIME_MS = 450; + + /** + * When creating the starting window, we use the exact same layout flags such that we end up + * with a window with the exact same dimensions etc. However, these flags are not used in layout + * and might cause other side effects so we exclude them. + */ + static final int FLAG_INHERIT_EXCLUDES = FLAG_NOT_FOCUSABLE + | FLAG_NOT_TOUCHABLE + | FLAG_NOT_TOUCH_MODAL + | FLAG_ALT_FOCUSABLE_IM + | FLAG_NOT_FOCUSABLE + | FLAG_HARDWARE_ACCELERATED + | FLAG_IGNORE_CHEEK_PRESSES + | FLAG_LOCAL_FOCUS_MODE + | FLAG_SLIPPERY + | FLAG_WATCH_OUTSIDE_TOUCH + | FLAG_SPLIT_TOUCH + | FLAG_SCALED + | FLAG_SECURE; + + private static final String TAG = StartingSurfaceDrawer.TAG; + private static final boolean DEBUG = StartingSurfaceDrawer.DEBUG_TASK_SNAPSHOT; + private static final String TITLE_FORMAT = "SnapshotStartingWindow for taskId=%s"; + + //tmp vars for unused relayout params + private static final Point TMP_SURFACE_SIZE = new Point(); + + private final Window mWindow; + private final Surface mSurface; + private final Runnable mClearWindowHandler; + private SurfaceControl mSurfaceControl; + private SurfaceControl mChildSurfaceControl; + private final IWindowSession mSession; + private final Rect mTaskBounds; + private final Rect mFrame = new Rect(); + private final Rect mSystemBarInsets = new Rect(); + private TaskSnapshot mSnapshot; + private final RectF mTmpSnapshotSize = new RectF(); + private final RectF mTmpDstFrame = new RectF(); + private final CharSequence mTitle; + private boolean mHasDrawn; + private long mShownTime; + private boolean mSizeMismatch; + private final Paint mBackgroundPaint = new Paint(); + private final int mActivityType; + private final int mStatusBarColor; + private final SystemBarBackgroundPainter mSystemBarBackgroundPainter; + private final int mOrientationOnCreation; + private final SurfaceControl.Transaction mTransaction; + private final Matrix mSnapshotMatrix = new Matrix(); + private final float[] mTmpFloat9 = new float[9]; + + static TaskSnapshotWindow create(StartingWindowInfo info, IBinder appToken, + TaskSnapshot snapshot, ShellExecutor mainExecutor, Runnable clearWindowHandler) { + final ActivityManager.RunningTaskInfo runningTaskInfo = info.taskInfo; + final int taskId = runningTaskInfo.taskId; + if (DEBUG) { + Slog.d(TAG, "create taskSnapshot surface for task: " + taskId); + } + + final WindowManager.LayoutParams attrs = info.topOpaqueWindowLayoutParams; + final WindowManager.LayoutParams mainWindowParams = info.mainWindowLayoutParams; + final InsetsState topWindowInsetsState = info.topOpaqueWindowInsetsState; + if (attrs == null || mainWindowParams == null || topWindowInsetsState == null) { + Slog.w(TAG, "unable to create taskSnapshot surface for task: " + taskId); + return null; + } + final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); + + final int appearance = attrs.insetsFlags.appearance; + final int windowFlags = attrs.flags; + final int windowPrivateFlags = attrs.privateFlags; + + layoutParams.packageName = mainWindowParams.packageName; + layoutParams.windowAnimations = mainWindowParams.windowAnimations; + layoutParams.dimAmount = mainWindowParams.dimAmount; + layoutParams.type = TYPE_APPLICATION_STARTING; + layoutParams.format = snapshot.getHardwareBuffer().getFormat(); + layoutParams.flags = (windowFlags & ~FLAG_INHERIT_EXCLUDES) + | FLAG_NOT_FOCUSABLE + | FLAG_NOT_TOUCHABLE; + // Setting as trusted overlay to let touches pass through. This is safe because this + // window is controlled by the system. + layoutParams.privateFlags = (windowPrivateFlags & PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS) + | PRIVATE_FLAG_TRUSTED_OVERLAY; + layoutParams.token = appToken; + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.insetsFlags.appearance = appearance; + layoutParams.insetsFlags.behavior = attrs.insetsFlags.behavior; + layoutParams.layoutInDisplayCutoutMode = attrs.layoutInDisplayCutoutMode; + layoutParams.setFitInsetsTypes(attrs.getFitInsetsTypes()); + layoutParams.setFitInsetsSides(attrs.getFitInsetsSides()); + layoutParams.setFitInsetsIgnoringVisibility(attrs.isFitInsetsIgnoringVisibility()); + + layoutParams.setTitle(String.format(TITLE_FORMAT, taskId)); + + final Point taskSize = snapshot.getTaskSize(); + final Rect taskBounds = new Rect(0, 0, taskSize.x, taskSize.y); + final int orientation = snapshot.getOrientation(); + + final int activityType = runningTaskInfo.topActivityType; + final int displayId = runningTaskInfo.displayId; + + final IWindowSession session = WindowManagerGlobal.getWindowSession(); + final SurfaceControl surfaceControl = new SurfaceControl(); + final ClientWindowFrames tmpFrames = new ClientWindowFrames(); + + final InsetsSourceControl[] mTempControls = new InsetsSourceControl[0]; + final MergedConfiguration tmpMergedConfiguration = new MergedConfiguration(); + + final TaskDescription taskDescription; + if (runningTaskInfo.taskDescription != null) { + taskDescription = runningTaskInfo.taskDescription; + } else { + taskDescription = new TaskDescription(); + taskDescription.setBackgroundColor(WHITE); + } + + final TaskSnapshotWindow snapshotSurface = new TaskSnapshotWindow( + surfaceControl, snapshot, layoutParams.getTitle(), taskDescription, appearance, + windowFlags, windowPrivateFlags, taskBounds, orientation, activityType, + topWindowInsetsState, clearWindowHandler); + final Window window = snapshotSurface.mWindow; + + final InsetsState mTmpInsetsState = new InsetsState(); + final InputChannel tmpInputChannel = new InputChannel(); + mainExecutor.execute(() -> { + try { + final int res = session.addToDisplay(window, layoutParams, View.GONE, displayId, + mTmpInsetsState, tmpInputChannel, mTmpInsetsState, mTempControls); + if (res < 0) { + Slog.w(TAG, "Failed to add snapshot starting window res=" + res); + return; + } + } catch (RemoteException e) { + snapshotSurface.clearWindowSynced(); + } + window.setOuter(snapshotSurface, mainExecutor); + try { + session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, -1, + tmpFrames, tmpMergedConfiguration, surfaceControl, mTmpInsetsState, + mTempControls, TMP_SURFACE_SIZE); + } catch (RemoteException e) { + snapshotSurface.clearWindowSynced(); + } + + final Rect systemBarInsets = getSystemBarInsets(tmpFrames.frame, topWindowInsetsState); + snapshotSurface.setFrames(tmpFrames.frame, systemBarInsets); + snapshotSurface.drawSnapshot(); + }); + return snapshotSurface; + } + + public TaskSnapshotWindow(SurfaceControl surfaceControl, + TaskSnapshot snapshot, CharSequence title, TaskDescription taskDescription, + int appearance, int windowFlags, int windowPrivateFlags, Rect taskBounds, + int currentOrientation, int activityType, InsetsState topWindowInsetsState, + Runnable clearWindowHandler) { + mSurface = new Surface(); + mSession = WindowManagerGlobal.getWindowSession(); + mWindow = new Window(); + mWindow.setSession(mSession); + mSurfaceControl = surfaceControl; + mSnapshot = snapshot; + mTitle = title; + int backgroundColor = taskDescription.getBackgroundColor(); + mBackgroundPaint.setColor(backgroundColor != 0 ? backgroundColor : WHITE); + mTaskBounds = taskBounds; + mSystemBarBackgroundPainter = new SystemBarBackgroundPainter(windowFlags, + windowPrivateFlags, appearance, taskDescription, 1f, topWindowInsetsState); + mStatusBarColor = taskDescription.getStatusBarColor(); + mOrientationOnCreation = currentOrientation; + mActivityType = activityType; + mTransaction = new SurfaceControl.Transaction(); + mClearWindowHandler = clearWindowHandler; + } + + /** + * Ask system bar background painter to draw status bar background. + * @hide + */ + public void drawStatusBarBackground(Canvas c, @Nullable Rect alreadyDrawnFrame) { + mSystemBarBackgroundPainter.drawStatusBarBackground(c, alreadyDrawnFrame, + mSystemBarBackgroundPainter.getStatusBarColorViewHeight()); + } + + /** + * Ask system bar background painter to draw navigation bar background. + * @hide + */ + public void drawNavigationBarBackground(Canvas c) { + mSystemBarBackgroundPainter.drawNavigationBarBackground(c); + } + + void remove(ShellExecutor mainExecutor) { + final long now = SystemClock.uptimeMillis(); + if (mSizeMismatch && now - mShownTime < SIZE_MISMATCH_MINIMUM_TIME_MS + // Show the latest content as soon as possible for unlocking to home. + && mActivityType != ACTIVITY_TYPE_HOME) { + final long delayTime = mShownTime + SIZE_MISMATCH_MINIMUM_TIME_MS - now; + mainExecutor.executeDelayed(() -> remove(mainExecutor), delayTime); + if (DEBUG) { + Slog.d(TAG, "Defer removing snapshot surface in " + delayTime); + } + return; + } + mainExecutor.execute(() -> { + try { + if (DEBUG) { + Slog.d(TAG, "Removing snapshot surface, mHasDrawn: " + mHasDrawn); + } + mSession.remove(mWindow); + } catch (RemoteException e) { + // nothing + } + }); + } + + /** + * Set frame size. + * @hide + */ + public void setFrames(Rect frame, Rect systemBarInsets) { + mFrame.set(frame); + mSystemBarInsets.set(systemBarInsets); + final HardwareBuffer snapshot = mSnapshot.getHardwareBuffer(); + mSizeMismatch = (mFrame.width() != snapshot.getWidth() + || mFrame.height() != snapshot.getHeight()); + mSystemBarBackgroundPainter.setInsets(systemBarInsets); + } + + static Rect getSystemBarInsets(Rect frame, InsetsState state) { + return state.calculateInsets(frame, WindowInsets.Type.systemBars(), + false /* ignoreVisibility */); + } + + private void drawSnapshot() { + mSurface.copyFrom(mSurfaceControl); + if (DEBUG) { + Slog.d(TAG, "Drawing snapshot surface sizeMismatch= " + mSizeMismatch); + } + if (mSizeMismatch) { + // The dimensions of the buffer and the window don't match, so attaching the buffer + // will fail. Better create a child window with the exact dimensions and fill the parent + // window with the background color! + drawSizeMismatchSnapshot(); + } else { + drawSizeMatchSnapshot(); + } + mShownTime = SystemClock.uptimeMillis(); + mHasDrawn = true; + reportDrawn(); + + // In case window manager leaks us, make sure we don't retain the snapshot. + mSnapshot = null; + } + + private void drawSizeMatchSnapshot() { + mSurface.attachAndQueueBufferWithColorSpace(mSnapshot.getHardwareBuffer(), + mSnapshot.getColorSpace()); + mSurface.release(); + } + + private void drawSizeMismatchSnapshot() { + if (!mSurface.isValid()) { + throw new IllegalStateException("mSurface does not hold a valid surface."); + } + final HardwareBuffer buffer = mSnapshot.getHardwareBuffer(); + final SurfaceSession session = new SurfaceSession(); + + // We consider nearly matched dimensions as there can be rounding errors and the user won't + // notice very minute differences from scaling one dimension more than the other + final boolean aspectRatioMismatch = Math.abs( + ((float) buffer.getWidth() / buffer.getHeight()) + - ((float) mFrame.width() / mFrame.height())) > 0.01f; + + // Keep a reference to it such that it doesn't get destroyed when finalized. + mChildSurfaceControl = new SurfaceControl.Builder(session) + .setName(mTitle + " - task-snapshot-surface") + .setBufferSize(buffer.getWidth(), buffer.getHeight()) + .setFormat(buffer.getFormat()) + .setParent(mSurfaceControl) + .setCallsite("TaskSnapshotWindow.drawSizeMismatchSnapshot") + .build(); + Surface surface = new Surface(); + surface.copyFrom(mChildSurfaceControl); + + final Rect frame; + // We can just show the surface here as it will still be hidden as the parent is + // still hidden. + mTransaction.show(mChildSurfaceControl); + if (aspectRatioMismatch) { + // Clip off ugly navigation bar. + final Rect crop = calculateSnapshotCrop(); + frame = calculateSnapshotFrame(crop); + mTransaction.setWindowCrop(mChildSurfaceControl, crop); + mTransaction.setPosition(mChildSurfaceControl, frame.left, frame.top); + mTmpSnapshotSize.set(crop); + mTmpDstFrame.set(frame); + } else { + frame = null; + mTmpSnapshotSize.set(0, 0, buffer.getWidth(), buffer.getHeight()); + mTmpDstFrame.set(mFrame); + mTmpDstFrame.offsetTo(0, 0); + } + + // Scale the mismatch dimensions to fill the task bounds + mSnapshotMatrix.setRectToRect(mTmpSnapshotSize, mTmpDstFrame, Matrix.ScaleToFit.FILL); + mTransaction.setMatrix(mChildSurfaceControl, mSnapshotMatrix, mTmpFloat9); + + mTransaction.apply(); + surface.attachAndQueueBufferWithColorSpace(buffer, mSnapshot.getColorSpace()); + surface.release(); + + if (aspectRatioMismatch) { + final Canvas c = mSurface.lockCanvas(null); + drawBackgroundAndBars(c, frame); + mSurface.unlockCanvasAndPost(c); + mSurface.release(); + } + } + + /** + * Calculates the snapshot crop in snapshot coordinate space. + * + * @return crop rect in snapshot coordinate space. + */ + public Rect calculateSnapshotCrop() { + final Rect rect = new Rect(); + final HardwareBuffer snapshot = mSnapshot.getHardwareBuffer(); + rect.set(0, 0, snapshot.getWidth(), snapshot.getHeight()); + final Rect insets = mSnapshot.getContentInsets(); + + final float scaleX = (float) snapshot.getWidth() / mSnapshot.getTaskSize().x; + final float scaleY = (float) snapshot.getHeight() / mSnapshot.getTaskSize().y; + + // Let's remove all system decorations except the status bar, but only if the task is at the + // very top of the screen. + final boolean isTop = mTaskBounds.top == 0 && mFrame.top == 0; + rect.inset((int) (insets.left * scaleX), + isTop ? 0 : (int) (insets.top * scaleY), + (int) (insets.right * scaleX), + (int) (insets.bottom * scaleY)); + return rect; + } + + /** + * Calculates the snapshot frame in window coordinate space from crop. + * + * @param crop rect that is in snapshot coordinate space. + */ + public Rect calculateSnapshotFrame(Rect crop) { + final HardwareBuffer snapshot = mSnapshot.getHardwareBuffer(); + final float scaleX = (float) snapshot.getWidth() / mSnapshot.getTaskSize().x; + final float scaleY = (float) snapshot.getHeight() / mSnapshot.getTaskSize().y; + + // Rescale the frame from snapshot to window coordinate space + final Rect frame = new Rect(0, 0, + (int) (crop.width() / scaleX + 0.5f), + (int) (crop.height() / scaleY + 0.5f) + ); + + // However, we also need to make space for the navigation bar on the left side. + frame.offset(mSystemBarInsets.left, 0); + return frame; + } + + /** + * Draw status bar and navigation bar background. + * @hide + */ + public void drawBackgroundAndBars(Canvas c, Rect frame) { + final int statusBarHeight = mSystemBarBackgroundPainter.getStatusBarColorViewHeight(); + final boolean fillHorizontally = c.getWidth() > frame.right; + final boolean fillVertically = c.getHeight() > frame.bottom; + if (fillHorizontally) { + c.drawRect(frame.right, alpha(mStatusBarColor) == 0xFF ? statusBarHeight : 0, + c.getWidth(), fillVertically + ? frame.bottom + : c.getHeight(), + mBackgroundPaint); + } + if (fillVertically) { + c.drawRect(0, frame.bottom, c.getWidth(), c.getHeight(), mBackgroundPaint); + } + mSystemBarBackgroundPainter.drawDecors(c, frame); + } + + /** + * Clear window from drawer, must be post on main executor. + */ + private void clearWindowSynced() { + if (mClearWindowHandler != null) { + mClearWindowHandler.run(); + } + } + + private void reportDrawn() { + try { + mSession.finishDrawing(mWindow, null /* postDrawTransaction */); + } catch (RemoteException e) { + clearWindowSynced(); + } + } + + static class Window extends BaseIWindow { + private TaskSnapshotWindow mOuter; + private ShellExecutor mMainExecutor; + + public void setOuter(TaskSnapshotWindow outer, ShellExecutor mainExecutor) { + mOuter = outer; + mMainExecutor = mainExecutor; + } + + @Override + public void resized(ClientWindowFrames frames, boolean reportDraw, + MergedConfiguration mergedConfiguration, boolean forceLayout, + boolean alwaysConsumeSystemBars, int displayId) { + if (mOuter != null) { + if (mergedConfiguration != null + && mOuter.mOrientationOnCreation + != mergedConfiguration.getMergedConfiguration().orientation) { + // The orientation of the screen is changing. We better remove the snapshot ASAP + // as we are going to wait on the new window in any case to unfreeze the screen, + // and the starting window is not needed anymore. + mMainExecutor.execute(() -> { + mOuter.clearWindowSynced(); + }); + } else if (reportDraw) { + mMainExecutor.execute(() -> { + if (mOuter.mHasDrawn) { + mOuter.reportDrawn(); + } + }); + } + } + } + } + + /** + * Helper class to draw the background of the system bars in regions the task snapshot isn't + * filling the window. + */ + static class SystemBarBackgroundPainter { + private final Paint mStatusBarPaint = new Paint(); + private final Paint mNavigationBarPaint = new Paint(); + private final int mStatusBarColor; + private final int mNavigationBarColor; + private final int mWindowFlags; + private final int mWindowPrivateFlags; + private final float mScale; + private final InsetsState mInsetsState; + private final Rect mSystemBarInsets = new Rect(); + + SystemBarBackgroundPainter(int windowFlags, int windowPrivateFlags, int appearance, + TaskDescription taskDescription, float scale, InsetsState insetsState) { + mWindowFlags = windowFlags; + mWindowPrivateFlags = windowPrivateFlags; + mScale = scale; + final Context context = ActivityThread.currentActivityThread().getSystemUiContext(); + final int semiTransparent = context.getColor( + R.color.system_bar_background_semi_transparent); + mStatusBarColor = DecorView.calculateBarColor(windowFlags, FLAG_TRANSLUCENT_STATUS, + semiTransparent, taskDescription.getStatusBarColor(), appearance, + APPEARANCE_LIGHT_STATUS_BARS, + taskDescription.getEnsureStatusBarContrastWhenTransparent()); + mNavigationBarColor = DecorView.calculateBarColor(windowFlags, + FLAG_TRANSLUCENT_NAVIGATION, semiTransparent, + taskDescription.getNavigationBarColor(), appearance, + APPEARANCE_LIGHT_NAVIGATION_BARS, + taskDescription.getEnsureNavigationBarContrastWhenTransparent() + && context.getResources().getBoolean(R.bool.config_navBarNeedsScrim)); + mStatusBarPaint.setColor(mStatusBarColor); + mNavigationBarPaint.setColor(mNavigationBarColor); + mInsetsState = insetsState; + } + + void setInsets(Rect systemBarInsets) { + mSystemBarInsets.set(systemBarInsets); + } + + int getStatusBarColorViewHeight() { + final boolean forceBarBackground = + (mWindowPrivateFlags & PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS) != 0; + if (STATUS_BAR_COLOR_VIEW_ATTRIBUTES.isVisible( + mInsetsState, mStatusBarColor, mWindowFlags, forceBarBackground)) { + return (int) (mSystemBarInsets.top * mScale); + } else { + return 0; + } + } + + private boolean isNavigationBarColorViewVisible() { + final boolean forceBarBackground = + (mWindowPrivateFlags & PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS) != 0; + return NAVIGATION_BAR_COLOR_VIEW_ATTRIBUTES.isVisible( + mInsetsState, mNavigationBarColor, mWindowFlags, forceBarBackground); + } + + void drawDecors(Canvas c, @Nullable Rect alreadyDrawnFrame) { + drawStatusBarBackground(c, alreadyDrawnFrame, getStatusBarColorViewHeight()); + drawNavigationBarBackground(c); + } + + void drawStatusBarBackground(Canvas c, @Nullable Rect alreadyDrawnFrame, + int statusBarHeight) { + if (statusBarHeight > 0 && Color.alpha(mStatusBarColor) != 0 + && (alreadyDrawnFrame == null || c.getWidth() > alreadyDrawnFrame.right)) { + final int rightInset = (int) (mSystemBarInsets.right * mScale); + final int left = alreadyDrawnFrame != null ? alreadyDrawnFrame.right : 0; + c.drawRect(left, 0, c.getWidth() - rightInset, statusBarHeight, mStatusBarPaint); + } + } + + @VisibleForTesting + void drawNavigationBarBackground(Canvas c) { + final Rect navigationBarRect = new Rect(); + getNavigationBarRect(c.getWidth(), c.getHeight(), mSystemBarInsets, navigationBarRect, + mScale); + final boolean visible = isNavigationBarColorViewVisible(); + if (visible && Color.alpha(mNavigationBarColor) != 0 && !navigationBarRect.isEmpty()) { + c.drawRect(navigationBarRect, mNavigationBarPaint); + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java new file mode 100644 index 000000000000..59f8c1df1213 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.transition; + +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.IBinder; +import android.util.ArrayMap; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TransactionPool; + +import java.util.ArrayList; + +/** The default handler that handles anything not already handled. */ +public class DefaultTransitionHandler implements Transitions.TransitionHandler { + private final TransactionPool mTransactionPool; + private final ShellExecutor mMainExecutor; + private final ShellExecutor mAnimExecutor; + + /** Keeps track of the currently-running animations associated with each transition. */ + private final ArrayMap<IBinder, ArrayList<Animator>> mAnimations = new ArrayMap<>(); + + DefaultTransitionHandler(@NonNull TransactionPool transactionPool, + @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { + mTransactionPool = transactionPool; + mMainExecutor = mainExecutor; + mAnimExecutor = animExecutor; + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (mAnimations.containsKey(transition)) { + throw new IllegalStateException("Got a duplicate startAnimation call for " + + transition); + } + final ArrayList<Animator> animations = new ArrayList<>(); + mAnimations.put(transition, animations); + final boolean isOpening = Transitions.isOpeningType(info.getType()); + + final Runnable onAnimFinish = () -> { + if (!animations.isEmpty()) return; + mAnimations.remove(transition); + finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); + }; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + + // Don't animate anything with an animating parent + if (change.getParent() != null) continue; + + final int mode = change.getMode(); + if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) { + if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) { + // This received a transferred starting window, so don't animate + continue; + } + // fade in + startExampleAnimation( + animations, change.getLeash(), true /* show */, onAnimFinish); + } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) { + // fade out + startExampleAnimation( + animations, change.getLeash(), false /* show */, onAnimFinish); + } + } + t.apply(); + // run finish now in-case there are no animations + onAnimFinish.run(); + return true; + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + return null; + } + + // TODO(shell-transitions): real animations + private void startExampleAnimation(@NonNull ArrayList<Animator> animations, + @NonNull SurfaceControl leash, boolean show, @NonNull Runnable finishCallback) { + 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(() -> { + animations.remove(va); + finishCallback.run(); + }); + }; + 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) { } + }); + animations.add(va); + mAnimExecutor.execute(va::start); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java new file mode 100644 index 000000000000..8271b0689053 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.transition; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Log; +import android.util.Pair; +import android.view.SurfaceControl; +import android.window.IRemoteTransition; +import android.window.IRemoteTransitionFinishedCallback; +import android.window.TransitionFilter; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.common.ShellExecutor; + +import java.util.ArrayList; + +/** + * Handler that deals with RemoteTransitions. It will only request to handle a transition + * if the request includes a specific remote. + */ +public class RemoteTransitionHandler implements Transitions.TransitionHandler { + private final ShellExecutor mMainExecutor; + + /** Includes remotes explicitly requested by, eg, ActivityOptions */ + private final ArrayMap<IBinder, IRemoteTransition> mPendingRemotes = new ArrayMap<>(); + + /** Ordered by specificity. Last filters will be checked first */ + private final ArrayList<Pair<TransitionFilter, IRemoteTransition>> mFilters = + new ArrayList<>(); + + RemoteTransitionHandler(@NonNull ShellExecutor mainExecutor) { + mMainExecutor = mainExecutor; + } + + void addFiltered(TransitionFilter filter, IRemoteTransition remote) { + mFilters.add(new Pair<>(filter, remote)); + } + + void removeFiltered(IRemoteTransition remote) { + for (int i = mFilters.size() - 1; i >= 0; --i) { + if (mFilters.get(i).second == remote) { + mFilters.remove(i); + } + } + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + IRemoteTransition pendingRemote = mPendingRemotes.remove(transition); + if (pendingRemote == null) { + // If no explicit remote, search filters until one matches + for (int i = mFilters.size() - 1; i >= 0; --i) { + if (mFilters.get(i).first.matches(info)) { + pendingRemote = mFilters.get(i).second; + break; + } + } + } + + if (pendingRemote == null) return false; + + final IRemoteTransition remote = pendingRemote; + final IBinder.DeathRecipient remoteDied = () -> { + Log.e(Transitions.TAG, "Remote transition died, finishing"); + mMainExecutor.execute( + () -> finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */)); + }; + IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() { + @Override + public void onTransitionFinished(WindowContainerTransaction wct) { + if (remote.asBinder() != null) { + remote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */); + } + mMainExecutor.execute( + () -> finishCallback.onTransitionFinished(wct, null /* wctCB */)); + } + }; + try { + if (remote.asBinder() != null) { + remote.asBinder().linkToDeath(remoteDied, 0 /* flags */); + } + remote.startAnimation(info, t, cb); + } catch (RemoteException e) { + if (remote.asBinder() != null) { + remote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */); + } + Log.e(Transitions.TAG, "Error running remote transition.", e); + mMainExecutor.execute( + () -> finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */)); + } + return true; + } + + @Override + @Nullable + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @Nullable TransitionRequestInfo request) { + IRemoteTransition remote = request.getRemoteTransition(); + if (remote == null) return null; + mPendingRemotes.put(transition, remote); + return new WindowContainerTransaction(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java new file mode 100644 index 000000000000..2ab4e0bdd76f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.transition; + +import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.util.ArrayMap; +import android.util.Log; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.IRemoteTransition; +import android.window.ITransitionPlayer; +import android.window.TransitionFilter; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; +import android.window.WindowContainerTransactionCallback; +import android.window.WindowOrganizer; + +import androidx.annotation.BinderThread; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.util.ArrayList; +import java.util.Arrays; + +/** Plays transition animations */ +public class Transitions { + 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 ShellExecutor mMainExecutor; + private final ShellExecutor mAnimExecutor; + private final TransitionPlayerImpl mPlayerImpl; + private final RemoteTransitionHandler mRemoteTransitionHandler; + + /** List of possible handlers. Ordered by specificity (eg. tapped back to front). */ + private final ArrayList<TransitionHandler> mHandlers = new ArrayList<>(); + + private static final class ActiveTransition { + TransitionHandler mFirstHandler = null; + } + + /** Keeps track of currently tracked transitions and all the animations associated with each */ + private final ArrayMap<IBinder, ActiveTransition> mActiveTransitions = new ArrayMap<>(); + + public Transitions(@NonNull WindowOrganizer organizer, @NonNull TransactionPool pool, + @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { + mOrganizer = organizer; + mMainExecutor = mainExecutor; + mAnimExecutor = animExecutor; + mPlayerImpl = new TransitionPlayerImpl(); + // The very last handler (0 in the list) should be the default one. + mHandlers.add(new DefaultTransitionHandler(pool, mainExecutor, animExecutor)); + // Next lowest priority is remote transitions. + mRemoteTransitionHandler = new RemoteTransitionHandler(mainExecutor); + mHandlers.add(mRemoteTransitionHandler); + } + + private Transitions() { + mOrganizer = null; + mMainExecutor = null; + mAnimExecutor = null; + mPlayerImpl = null; + mRemoteTransitionHandler = null; + } + + /** Create an empty/non-registering transitions object for system-ui tests. */ + @VisibleForTesting + public static Transitions createEmptyForTesting() { + return new Transitions(); + } + + /** Register this transition handler with Core */ + public void register(ShellTaskOrganizer taskOrganizer) { + if (mPlayerImpl == null) return; + taskOrganizer.registerTransitionPlayer(mPlayerImpl); + } + + /** + * Adds a handler candidate. + * @see TransitionHandler + */ + public void addHandler(@NonNull TransitionHandler handler) { + mHandlers.add(handler); + } + + public ShellExecutor getMainExecutor() { + return mMainExecutor; + } + + public ShellExecutor getAnimExecutor() { + return mAnimExecutor; + } + + /** Only use this in tests. This is used to avoid running animations during tests. */ + @VisibleForTesting + void replaceDefaultHandlerForTest(TransitionHandler handler) { + mHandlers.set(0, handler); + } + + /** Register a remote transition to be used when `filter` matches an incoming transition */ + @ExternalThread + public void registerRemote(@NonNull TransitionFilter filter, + @NonNull IRemoteTransition remoteTransition) { + mMainExecutor.execute(() -> mRemoteTransitionHandler.addFiltered(filter, remoteTransition)); + } + + /** Unregisters a remote transition and all associated filters */ + @ExternalThread + public void unregisterRemote(@NonNull IRemoteTransition remoteTransition) { + mMainExecutor.execute(() -> mRemoteTransitionHandler.removeFiltered(remoteTransition)); + } + + /** @return true if the transition was triggered by opening something vs closing something */ + public static boolean isOpeningType(@WindowManager.TransitionType int type) { + return type == TRANSIT_OPEN + || type == TRANSIT_TO_FRONT + || type == WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; + } + + /** + * Reparents all participants into a shared parent and orders them based on: the global transit + * type, their transit mode, and their destination z-order. + */ + private static void setupStartState(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t) { + boolean isOpening = isOpeningType(info.getType()); + if (info.getRootLeash().isValid()) { + t.show(info.getRootLeash()); + } + // Put animating stuff above this line and put static stuff below it. + int zSplitLine = info.getChanges().size(); + // changes should be ordered top-to-bottom in z + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + final SurfaceControl leash = change.getLeash(); + final int mode = info.getChanges().get(i).getMode(); + + // Don't move anything with an animating parent + if (change.getParent() != null) { + if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT || mode == TRANSIT_CHANGE) { + t.show(leash); + t.setMatrix(leash, 1, 0, 0, 1); + t.setAlpha(leash, 1.f); + t.setPosition(leash, change.getEndRelOffset().x, change.getEndRelOffset().y); + } + continue; + } + + t.reparent(leash, info.getRootLeash()); + t.setPosition(leash, change.getStartAbsBounds().left - info.getRootOffset().x, + change.getStartAbsBounds().top - info.getRootOffset().y); + // Put all the OPEN/SHOW on top + if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { + t.show(leash); + t.setMatrix(leash, 1, 0, 0, 1); + if (isOpening) { + // put on top with 0 alpha + t.setLayer(leash, zSplitLine + info.getChanges().size() - i); + if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) { + // This received a transferred starting window, so make it immediately + // visible. + t.setAlpha(leash, 1.f); + } else { + t.setAlpha(leash, 0.f); + } + } else { + // put on bottom and leave it visible + t.setLayer(leash, zSplitLine - i); + t.setAlpha(leash, 1.f); + } + } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) { + if (isOpening) { + // put on bottom and leave visible + t.setLayer(leash, zSplitLine - i); + } else { + // put on top + t.setLayer(leash, zSplitLine + info.getChanges().size() - i); + } + } else { // CHANGE + t.setLayer(leash, zSplitLine + info.getChanges().size() - i); + } + } + } + + @VisibleForTesting + void onTransitionReady(@NonNull IBinder transitionToken, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady %s: %s", + transitionToken, info); + final ActiveTransition active = mActiveTransitions.get(transitionToken); + if (active == null) { + throw new IllegalStateException("Got transitionReady for non-active transition " + + transitionToken + ". expecting one of " + + Arrays.toString(mActiveTransitions.keySet().toArray())); + } + if (!info.getRootLeash().isValid()) { + // Invalid root-leash implies that the transition is empty/no-op, so just do + // housekeeping and return. + t.apply(); + onFinish(transitionToken, null /* wct */, null /* wctCB */); + return; + } + + setupStartState(info, t); + + final TransitionFinishCallback finishCb = (wct, cb) -> onFinish(transitionToken, wct, cb); + // If a handler chose to uniquely run this animation, try delegating to it. + if (active.mFirstHandler != null && active.mFirstHandler.startAnimation( + transitionToken, info, t, finishCb)) { + return; + } + // Otherwise give every other handler a chance (in order) + for (int i = mHandlers.size() - 1; i >= 0; --i) { + if (mHandlers.get(i) == active.mFirstHandler) continue; + if (mHandlers.get(i).startAnimation(transitionToken, info, t, finishCb)) { + return; + } + } + throw new IllegalStateException( + "This shouldn't happen, maybe the default handler is broken."); + } + + private void onFinish(IBinder transition, @Nullable WindowContainerTransaction wct, + @Nullable WindowContainerTransactionCallback wctCB) { + if (!mActiveTransitions.containsKey(transition)) { + Log.e(TAG, "Trying to finish a non-running transition. Maybe remote crashed?"); + return; + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "Transition animations finished, notifying core %s", transition); + mActiveTransitions.remove(transition); + mOrganizer.finishTransition(transition, wct, wctCB); + } + + void requestStartTransition(@NonNull IBinder transitionToken, + @Nullable TransitionRequestInfo request) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition requested: %s %s", + transitionToken, request); + if (mActiveTransitions.containsKey(transitionToken)) { + throw new RuntimeException("Transition already started " + transitionToken); + } + final ActiveTransition active = new ActiveTransition(); + WindowContainerTransaction wct = null; + for (int i = mHandlers.size() - 1; i >= 0; --i) { + wct = mHandlers.get(i).handleRequest(transitionToken, request); + if (wct != null) { + active.mFirstHandler = mHandlers.get(i); + break; + } + } + IBinder transition = mOrganizer.startTransition( + request.getType(), transitionToken, wct); + mActiveTransitions.put(transition, active); + } + + /** Start a new transition directly. */ + public IBinder startTransition(@WindowManager.TransitionType int type, + @NonNull WindowContainerTransaction wct, @Nullable TransitionHandler handler) { + final ActiveTransition active = new ActiveTransition(); + active.mFirstHandler = handler; + IBinder transition = mOrganizer.startTransition(type, null /* token */, wct); + mActiveTransitions.put(transition, active); + return transition; + } + + /** + * Interface for a callback that must be called after a TransitionHandler finishes playing an + * animation. + */ + public interface TransitionFinishCallback { + /** + * This must be called on the main thread when a transition finishes playing an animation. + * The transition must not touch the surfaces after this has been called. + * + * @param wct A WindowContainerTransaction to run along with the transition clean-up. + * @param wctCB A sync callback that will be run when the transition clean-up is done and + * wct has been applied. + */ + void onTransitionFinished(@Nullable WindowContainerTransaction wct, + @Nullable WindowContainerTransactionCallback wctCB); + } + + /** + * Interface for something which can handle a subset of transitions. + */ + public interface TransitionHandler { + /** + * Starts a transition animation. This is always called if handleRequest returned non-null + * for a particular transition. Otherwise, it is only called if no other handler before + * it handled the transition. + * + * @param finishCallback Call this when finished. This MUST be called on main thread. + * @return true if transition was handled, false if not (falls-back to default). + */ + boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, + @NonNull TransitionFinishCallback finishCallback); + + /** + * Potentially handles a startTransition request. + * + * @param transition The transition whose start is being requested. + * @param request Information about what is requested. + * @return WCT to apply with transition-start or null. If a WCT is returned here, this + * handler will be the first in line to animate. + */ + @Nullable + WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request); + } + + @BinderThread + private class TransitionPlayerImpl extends ITransitionPlayer.Stub { + @Override + public void onTransitionReady(IBinder iBinder, TransitionInfo transitionInfo, + SurfaceControl.Transaction transaction) throws RemoteException { + mMainExecutor.execute(() -> { + Transitions.this.onTransitionReady(iBinder, transitionInfo, transaction); + }); + } + + @Override + public void requestStartTransition(IBinder iBinder, + TransitionRequestInfo request) throws RemoteException { + mMainExecutor.execute(() -> Transitions.this.requestStartTransition(iBinder, request)); + } + } +} 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..a57ac35583b2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/Android.bp @@ -0,0 +1,38 @@ +// +// 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: "AndroidTest.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", + "wm-flicker-common-assertions", + "wm-flicker-common-app-helpers", + "platform-test-annotations", + "wmshell-flicker-test-components", + ], +} diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml new file mode 100644 index 000000000000..e6d32ff1166f --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml @@ -0,0 +1,63 @@ +<?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" /> + <!-- Allow the test to write directly to /sdcard/ --> + <uses-permission android:name="android.permission.MANAGE_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"/> + <!-- Capture screen recording --> + <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT"/> + <!-- 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"/> + <!-- Force-stop test apps --> + <uses-permission android:name="android.permission.FORCE_STOP_PACKAGES"/> + <!-- Control test app's media session --> + <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/> + <!-- ATM.removeRootTasksWithActivityTypes() --> + <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" /> + <!-- Allow the test to write directly to /sdcard/ --> + <application android:requestLegacyExternalStorage="true"> + <uses-library android:name="android.test.runner"/> + + <service android:name=".NotificationListener" + android:exported="true" + android:label="WMShellTestsNotificationListenerService" + android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"> + <intent-filter> + <action android:name="android.service.notification.NotificationListenerService" /> + </intent-filter> + </service> + </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/AndroidTest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml new file mode 100644 index 000000000000..8258630a9502 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml @@ -0,0 +1,39 @@ +<?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" /> + <!-- 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="/sdcard/flicker" /> + <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..7ad75532eced --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt @@ -0,0 +1,178 @@ +/* + * 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.graphics.Region +import android.view.Surface +import com.android.server.wm.flicker.dsl.LayersAssertionBuilderLegacy +import com.android.server.wm.flicker.APP_PAIR_SPLIT_DIVIDER +import com.android.server.wm.flicker.DOCKED_STACK_DIVIDER +import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.server.wm.flicker.traces.layers.getVisibleBounds + +@JvmOverloads +fun LayersAssertionBuilderLegacy.appPairsDividerIsVisible( + bugId: Int = 0, + enabled: Boolean = bugId == 0 +) { + end("appPairsDividerIsVisible", bugId, enabled) { + this.isVisible(APP_PAIR_SPLIT_DIVIDER) + } +} + +@JvmOverloads +fun LayersAssertionBuilderLegacy.appPairsDividerIsInvisible( + bugId: Int = 0, + enabled: Boolean = bugId == 0 +) { + end("appPairsDividerIsInVisible", bugId, enabled) { + this.notExists(APP_PAIR_SPLIT_DIVIDER) + } +} + +@JvmOverloads +fun LayersAssertionBuilderLegacy.appPairsDividerBecomesVisible( + bugId: Int = 0, + enabled: Boolean = bugId == 0 +) { + all("dividerLayerBecomesVisible", bugId, enabled) { + this.hidesLayer(DOCKED_STACK_DIVIDER) + .then() + .showsLayer(DOCKED_STACK_DIVIDER) + } +} + +@JvmOverloads +fun LayersAssertionBuilderLegacy.dockedStackDividerIsVisible( + bugId: Int = 0, + enabled: Boolean = bugId == 0 +) { + end("dockedStackDividerIsVisible", bugId, enabled) { + this.isVisible(DOCKED_STACK_DIVIDER) + } +} + +@JvmOverloads +fun LayersAssertionBuilderLegacy.dockedStackDividerBecomesVisible( + bugId: Int = 0, + enabled: Boolean = bugId == 0 +) { + all("dividerLayerBecomesVisible", bugId, enabled) { + this.hidesLayer(DOCKED_STACK_DIVIDER) + .then() + .showsLayer(DOCKED_STACK_DIVIDER) + } +} + +@JvmOverloads +fun LayersAssertionBuilderLegacy.dockedStackDividerBecomesInvisible( + bugId: Int = 0, + enabled: Boolean = bugId == 0 +) { + all("dividerLayerBecomesInvisible", bugId, enabled) { + this.showsLayer(DOCKED_STACK_DIVIDER) + .then() + .hidesLayer(DOCKED_STACK_DIVIDER) + } +} + +@JvmOverloads +fun LayersAssertionBuilderLegacy.dockedStackDividerIsInvisible( + bugId: Int = 0, + enabled: Boolean = bugId == 0 +) { + end("dockedStackDividerIsInvisible", bugId, enabled) { + this.notExists(DOCKED_STACK_DIVIDER) + } +} + +@JvmOverloads +fun LayersAssertionBuilderLegacy.appPairsPrimaryBoundsIsVisible( + rotation: Int, + primaryLayerName: String, + bugId: Int = 0, + enabled: Boolean = bugId == 0 +) { + end("PrimaryAppBounds", bugId, enabled) { + val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER) + this.hasVisibleRegion(primaryLayerName, getPrimaryRegion(dividerRegion, rotation)) + } +} + +@JvmOverloads +fun LayersAssertionBuilderLegacy.appPairsSecondaryBoundsIsVisible( + rotation: Int, + secondaryLayerName: String, + bugId: Int = 0, + enabled: Boolean = bugId == 0 +) { + end("SecondaryAppBounds", bugId, enabled) { + val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER) + this.hasVisibleRegion(secondaryLayerName, getSecondaryRegion(dividerRegion, rotation)) + } +} + +@JvmOverloads +fun LayersAssertionBuilderLegacy.dockedStackPrimaryBoundsIsVisible( + rotation: Int, + primaryLayerName: String, + bugId: Int = 0, + enabled: Boolean = bugId == 0 +) { + end("PrimaryAppBounds", bugId, enabled) { + val dividerRegion = entry.getVisibleBounds(DOCKED_STACK_DIVIDER) + this.hasVisibleRegion(primaryLayerName, getPrimaryRegion(dividerRegion, rotation)) + } +} + +@JvmOverloads +fun LayersAssertionBuilderLegacy.dockedStackSecondaryBoundsIsVisible( + rotation: Int, + secondaryLayerName: String, + bugId: Int = 0, + enabled: Boolean = bugId == 0 +) { + end("SecondaryAppBounds", bugId, enabled) { + val dividerRegion = entry.getVisibleBounds(DOCKED_STACK_DIVIDER) + this.hasVisibleRegion(secondaryLayerName, getSecondaryRegion(dividerRegion, rotation)) + } +} + +fun getPrimaryRegion(dividerRegion: Region, rotation: Int): Region { + val displayBounds = WindowUtils.getDisplayBounds(rotation) + return if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) { + Region(0, 0, displayBounds.bounds.right, + dividerRegion.bounds.bottom - WindowUtils.dockedStackDividerInset) + } else { + Region(0, 0, dividerRegion.bounds.left, + dividerRegion.bounds.right - WindowUtils.dockedStackDividerInset) + } +} + +fun getSecondaryRegion(dividerRegion: Region, rotation: Int): Region { + val displayBounds = WindowUtils.getDisplayBounds(rotation) + return if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) { + Region(0, + dividerRegion.bounds.bottom - WindowUtils.dockedStackDividerInset, + displayBounds.bounds.right, + displayBounds.bounds.bottom - WindowUtils.dockedStackDividerInset) + } else { + Region(dividerRegion.bounds.right, 0, + displayBounds.bounds.right, + displayBounds.bounds.bottom - WindowUtils.dockedStackDividerInset) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt new file mode 100644 index 000000000000..d2cfb0fbb5f6 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt @@ -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. + */ + +package com.android.wm.shell.flicker + +const val IME_WINDOW_NAME = "InputMethod" +const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui" 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..89bbdb0a2f99 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker + +import android.content.pm.PackageManager +import android.content.pm.PackageManager.FEATURE_LEANBACK +import android.content.pm.PackageManager.FEATURE_LEANBACK_ONLY +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 +import org.junit.Assume.assumeFalse +import org.junit.Before + +/** + * 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) } + val packageManager: PackageManager by lazy { instrumentation.context.getPackageManager() } + protected val isTelevision: Boolean by lazy { + packageManager.run { + hasSystemFeature(FEATURE_LEANBACK) || hasSystemFeature(FEATURE_LEANBACK_ONLY) + } + } + + /** + * By default WmShellFlickerTests do not run on TV devices. + * If the test should run on TV - it should override this method. + */ + @Before + open fun televisionSetUp() = assumeFalse(isTelevision) + + /** + * 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) + } + } +} 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/NotificationListener.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NotificationListener.kt new file mode 100644 index 000000000000..51f7a18f60dd --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NotificationListener.kt @@ -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.flicker + +import android.service.notification.NotificationListenerService +import android.service.notification.StatusBarNotification +import android.util.Log +import com.android.compatibility.common.util.SystemUtil.runShellCommand + +class NotificationListener : NotificationListenerService() { + private val notifications: MutableMap<Any, StatusBarNotification> = mutableMapOf() + + override fun onNotificationPosted(sbn: StatusBarNotification) { + if (DEBUG) Log.d(TAG, "onNotificationPosted: $sbn") + notifications[sbn.key] = sbn + } + + override fun onNotificationRemoved(sbn: StatusBarNotification) { + if (DEBUG) Log.d(TAG, "onNotificationRemoved: $sbn") + notifications.remove(sbn.key) + } + + override fun onListenerConnected() { + if (DEBUG) Log.d(TAG, "onListenerConnected") + instance = this + } + + override fun onListenerDisconnected() { + if (DEBUG) Log.d(TAG, "onListenerDisconnected") + instance = null + notifications.clear() + } + + companion object { + private const val DEBUG = false + private const val TAG = "WMShellFlickerTests_NotificationListener" + + private const val CMD_NOTIFICATION_ALLOW_LISTENER = "cmd notification allow_listener %s" + private const val CMD_NOTIFICATION_DISALLOW_LISTENER = + "cmd notification disallow_listener %s" + private const val COMPONENT_NAME = "com.android.wm.shell.flicker/.NotificationListener" + + private var instance: NotificationListener? = null + + fun startNotificationListener(): Boolean { + if (instance != null) { + return true + } + + runShellCommand(CMD_NOTIFICATION_ALLOW_LISTENER.format(COMPONENT_NAME)) + return wait { instance != null } + } + + fun stopNotificationListener(): Boolean { + if (instance == null) { + return true + } + + runShellCommand(CMD_NOTIFICATION_DISALLOW_LISTENER.format(COMPONENT_NAME)) + return wait { instance == null } + } + + fun findNotification( + predicate: (StatusBarNotification) -> Boolean + ): StatusBarNotification? { + instance?.run { + return notifications.values.firstOrNull(predicate) + } ?: throw IllegalStateException("NotificationListenerService is not connected") + } + + fun waitForNotificationToAppear( + predicate: (StatusBarNotification) -> Boolean + ): StatusBarNotification? { + instance?.let { + return waitForResult(extractor = { + it.notifications.values.firstOrNull(predicate) + }).second + } ?: throw IllegalStateException("NotificationListenerService is not connected") + } + + fun waitForNotificationToDisappear( + predicate: (StatusBarNotification) -> Boolean + ): Boolean { + return instance?.let { + wait { it.notifications.values.none(predicate) } + } ?: throw IllegalStateException("NotificationListenerService is not connected") + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt new file mode 100644 index 000000000000..a6d67355f271 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.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 + +import android.os.SystemClock + +private const val DEFAULT_TIMEOUT = 10000L +private const val DEFAULT_POLL_INTERVAL = 1000L + +fun wait(condition: () -> Boolean): Boolean { + val (success, _) = waitForResult(extractor = condition, validator = { it }) + return success +} + +fun <R> waitForResult( + timeout: Long = DEFAULT_TIMEOUT, + interval: Long = DEFAULT_POLL_INTERVAL, + extractor: () -> R, + validator: (R) -> Boolean = { it != null } +): Pair<Boolean, R?> { + val startTime = SystemClock.uptimeMillis() + do { + val result = extractor() + if (validator(result)) { + return (true to result) + } + SystemClock.sleep(interval) + } while (SystemClock.uptimeMillis() - startTime < timeout) + + return (false to null) +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt new file mode 100644 index 000000000000..d25774935e86 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt @@ -0,0 +1,87 @@ +/* + * 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.apppairs + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import android.os.SystemClock +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.wm.shell.flicker.helpers.AppPairsHelper +import com.android.wm.shell.flicker.appPairsDividerIsInvisible +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test AppPairs launch. + * To run this test: `atest WMShellFlickerTests:AppPairsTest` + */ +/** + * Test cold launch app from launcher. + * To run this test: `atest WMShellFlickerTests:AppPairsTestCannotPairNonResizeableApps` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class AppPairsTestCannotPairNonResizeableApps( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : AppPairsTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<Array<Any>> { + val testTag = "testAppPairs_cannotPairNonResizeableApps" + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag(testTag, configuration) + } + transitions { + nonResizeableApp?.launchViaIntent(wmHelper) + // TODO pair apps through normal UX flow + executeShellCommand( + composePairsCommand(primaryTaskId, nonResizeableTaskId, pair = true)) + SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) + } + assertions { + layersTrace { + appPairsDividerIsInvisible() + } + windowManagerTrace { + end("onlyResizeableAppWindowVisible") { + val nonResizeableApp = nonResizeableApp + require(nonResizeableApp != null) { + "Non resizeable app not initialized" + } + isVisible(nonResizeableApp.defaultWindowName) + isInvisible(primaryApp.defaultWindowName) + } + } + } + } + + return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, + transition, testSpec, repetitions = AppPairsHelper.TEST_REPETITIONS) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt new file mode 100644 index 000000000000..257350b6950b --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt @@ -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.flicker.apppairs + +import android.os.Bundle +import android.os.SystemClock +import android.platform.test.annotations.Presubmit +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.APP_PAIR_SPLIT_DIVIDER +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.traces.layers.getVisibleBounds +import com.android.wm.shell.flicker.appPairsDividerIsVisible +import com.android.wm.shell.flicker.helpers.AppPairsHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test cold launch app from launcher. + * To run this test: `atest WMShellFlickerTests:AppPairsTestPairPrimaryAndSecondaryApps` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class AppPairsTestPairPrimaryAndSecondaryApps( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : AppPairsTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<Array<Any>> { + val testTag = "testAppPairs_pairPrimaryAndSecondaryApps" + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag(testTag, configuration) + } + transitions { + // TODO pair apps through normal UX flow + executeShellCommand( + composePairsCommand(primaryTaskId, secondaryTaskId, pair = true)) + SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) + } + assertions { + layersTrace { + appPairsDividerIsVisible() + end("appsEndingBounds", enabled = false) { + val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER) + this.hasVisibleRegion(primaryApp.defaultWindowName, + appPairsHelper.getPrimaryBounds(dividerRegion)) + .hasVisibleRegion(secondaryApp.defaultWindowName, + appPairsHelper.getSecondaryBounds(dividerRegion)) + } + } + windowManagerTrace { + end("bothAppWindowsVisible") { + isVisible(primaryApp.defaultWindowName) + isVisible(secondaryApp.defaultWindowName) + } + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, transition, + testSpec, repetitions = AppPairsHelper.TEST_REPETITIONS) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt new file mode 100644 index 000000000000..0b001f5ac1b6 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt @@ -0,0 +1,95 @@ +/* + * 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.apppairs + +import android.os.Bundle +import android.os.SystemClock +import android.platform.test.annotations.Presubmit +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.APP_PAIR_SPLIT_DIVIDER +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.traces.layers.getVisibleBounds +import com.android.wm.shell.flicker.appPairsDividerIsInvisible +import com.android.wm.shell.flicker.helpers.AppPairsHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test cold launch app from launcher. + * To run this test: `atest WMShellFlickerTests:AppPairsTestUnpairPrimaryAndSecondaryApps` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class AppPairsTestUnpairPrimaryAndSecondaryApps( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : AppPairsTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<Array<Any>> { + val testTag = "testAppPairs_unpairPrimaryAndSecondaryApps" + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag(testTag, configuration) + } + setup { + executeShellCommand( + composePairsCommand(primaryTaskId, secondaryTaskId, pair = true)) + SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) + } + transitions { + // TODO pair apps through normal UX flow + executeShellCommand( + composePairsCommand(primaryTaskId, secondaryTaskId, pair = false)) + SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) + } + assertions { + layersTrace { + appPairsDividerIsInvisible() + start("appsStartingBounds", enabled = false) { + val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER) + this.hasVisibleRegion(primaryApp.defaultWindowName, + appPairsHelper.getPrimaryBounds(dividerRegion)) + .hasVisibleRegion(secondaryApp.defaultWindowName, + appPairsHelper.getSecondaryBounds(dividerRegion)) + } + end("appsEndingBounds", enabled = false) { + this.notExists(primaryApp.defaultWindowName) + .notExists(secondaryApp.defaultWindowName) + } + } + windowManagerTrace { + end("bothAppWindowsInvisible") { + isInvisible(primaryApp.defaultWindowName) + isInvisible(secondaryApp.defaultWindowName) + } + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, transition, + testSpec, repetitions = AppPairsHelper.TEST_REPETITIONS) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt new file mode 100644 index 000000000000..78a938aef69e --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt @@ -0,0 +1,131 @@ +/* + * 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.apppairs + +import android.app.Instrumentation +import android.os.Bundle +import android.system.helpers.ActivityHelper +import android.util.Log +import com.android.compatibility.common.util.SystemUtil +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.helpers.AppPairsHelper +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import com.android.wm.shell.flicker.testapp.Components +import java.io.IOException + +open class AppPairsTransition( + protected val instrumentation: Instrumentation +) { + internal val activityHelper = ActivityHelper.getInstance() + + internal val appPairsHelper = AppPairsHelper(instrumentation, + Components.SplitScreenActivity.LABEL, + Components.SplitScreenActivity.COMPONENT) + + internal val primaryApp = SplitScreenHelper.getPrimary(instrumentation) + internal val secondaryApp = SplitScreenHelper.getSecondary(instrumentation) + internal open val nonResizeableApp: SplitScreenHelper? = + SplitScreenHelper.getNonResizeable(instrumentation) + internal var primaryTaskId = "" + internal var secondaryTaskId = "" + internal var nonResizeableTaskId = "" + + internal open val transition: FlickerBuilder.(Bundle) -> Unit + get() = { configuration -> + setup { + test { + device.wakeUpAndGoToHomeScreen() + } + eachRun { + this.setRotation(configuration.startRotation) + primaryApp.launchViaIntent(wmHelper) + secondaryApp.launchViaIntent(wmHelper) + nonResizeableApp?.launchViaIntent(wmHelper) + updateTasksId() + } + } + teardown { + eachRun { + executeShellCommand(composePairsCommand( + primaryTaskId, secondaryTaskId, pair = false)) + executeShellCommand(composePairsCommand( + primaryTaskId, nonResizeableTaskId, pair = false)) + primaryApp.exit() + secondaryApp.exit() + nonResizeableApp?.exit() + } + } + + assertions { + layersTrace { + navBarLayerIsAlwaysVisible() + statusBarLayerIsAlwaysVisible() + } + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + } + } + } + + protected fun updateTasksId() { + primaryTaskId = getTaskIdForActivity( + primaryApp.component.packageName, primaryApp.component.className).toString() + secondaryTaskId = getTaskIdForActivity( + secondaryApp.component.packageName, secondaryApp.component.className).toString() + val nonResizeableApp = nonResizeableApp + if (nonResizeableApp != null) { + nonResizeableTaskId = getTaskIdForActivity( + nonResizeableApp.component.packageName, + nonResizeableApp.component.className).toString() + } + } + + private fun getTaskIdForActivity(pkgName: String, activityName: String): Int { + return activityHelper.getTaskIdForActivity(pkgName, activityName) + } + + internal fun executeShellCommand(cmd: String) { + try { + SystemUtil.runShellCommand(instrumentation, cmd) + } catch (e: IOException) { + Log.d("AppPairsTest", "executeShellCommand error! $e") + } + } + + internal fun composePairsCommand( + primaryApp: String, + secondaryApp: String, + pair: Boolean + ): String = buildString { + // dumpsys activity service SystemUIService WMShell {pair|unpair} ${TASK_ID_1} ${TASK_ID_2} + append("dumpsys activity service SystemUIService WMShell ") + if (pair) { + append("pair ") + } else { + append("unpair ") + } + append("$primaryApp $secondaryApp") + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt new file mode 100644 index 000000000000..aafa9bfbd676 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.apppairs + +import android.os.Bundle +import android.os.SystemClock +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.appPairsDividerIsVisible +import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisible +import com.android.wm.shell.flicker.appPairsSecondaryBoundsIsVisible +import com.android.wm.shell.flicker.helpers.AppPairsHelper +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test open apps to app pairs and rotate. + * To run this test: `atest WMShellFlickerTests:RotateTwoLaunchedAppsInAppPairsMode` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class RotateTwoLaunchedAppsInAppPairsMode( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : RotateTwoLaunchedAppsTransition( + InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testRotateTwoLaunchedAppsInAppPairsMode", configuration) + } + transitions { + executeShellCommand(composePairsCommand( + primaryTaskId, secondaryTaskId, true /* pair */)) + SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) + setRotation(configuration.endRotation) + } + assertions { + layersTrace { + navBarLayerRotatesAndScales(Surface.ROTATION_0, configuration.endRotation, + enabled = false) + statusBarLayerRotatesScales(Surface.ROTATION_0, configuration.endRotation, + enabled = false) + appPairsDividerIsVisible(enabled = false) + appPairsPrimaryBoundsIsVisible(configuration.endRotation, + primaryApp.defaultWindowName, bugId = 172776659) + appPairsSecondaryBoundsIsVisible(configuration.endRotation, + secondaryApp.defaultWindowName, bugId = 172776659) + } + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + end("bothAppWindowsVisible") { + isVisible(primaryApp.defaultWindowName) + .isVisible(secondaryApp.defaultWindowName) + } + } + } + } + + return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, + transition, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_90, Surface.ROTATION_270)) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt new file mode 100644 index 000000000000..19ca31fbee4a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.apppairs + +import android.os.Bundle +import android.os.SystemClock +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.isRotated +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.appPairsDividerIsVisible +import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisible +import com.android.wm.shell.flicker.appPairsSecondaryBoundsIsVisible +import com.android.wm.shell.flicker.helpers.AppPairsHelper +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test open apps to app pairs and rotate. + * To run this test: `atest WMShellFlickerTests:RotateTwoLaunchedAppsRotateAndEnterAppPairsMode` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class RotateTwoLaunchedAppsRotateAndEnterAppPairsMode( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : RotateTwoLaunchedAppsTransition( + InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testRotateAndEnterAppPairsMode", configuration) + } + transitions { + this.setRotation(configuration.endRotation) + executeShellCommand( + composePairsCommand(primaryTaskId, secondaryTaskId, pair = true)) + SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) + } + assertions { + layersTrace { + navBarLayerRotatesAndScales(Surface.ROTATION_0, configuration.endRotation, + enabled = !configuration.startRotation.isRotated()) + statusBarLayerRotatesScales(Surface.ROTATION_0, configuration.endRotation) + appPairsDividerIsVisible() + appPairsPrimaryBoundsIsVisible(configuration.endRotation, + primaryApp.defaultWindowName, 172776659) + appPairsSecondaryBoundsIsVisible(configuration.endRotation, + secondaryApp.defaultWindowName, 172776659) + } + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + end("bothAppWindowsVisible") { + isVisible(primaryApp.defaultWindowName) + isVisible(secondaryApp.defaultWindowName) + } + } + } + } + + return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, + transition, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_90, Surface.ROTATION_270) + ) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt new file mode 100644 index 000000000000..8ea2544fcf61 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.apppairs + +import android.app.Instrumentation +import android.os.Bundle +import android.view.Surface +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.wm.shell.flicker.helpers.SplitScreenHelper + +open class RotateTwoLaunchedAppsTransition( + instrumentation: Instrumentation +) : AppPairsTransition(instrumentation) { + override val nonResizeableApp: SplitScreenHelper? + get() = null + + override val transition: FlickerBuilder.(Bundle) -> Unit + get() = { + setup { + test { + device.wakeUpAndGoToHomeScreen() + this.setRotation(Surface.ROTATION_0) + primaryApp.launchViaIntent() + secondaryApp.launchViaIntent() + updateTasksId() + } + } + teardown { + eachRun { + executeShellCommand(composePairsCommand( + primaryTaskId, secondaryTaskId, pair = false)) + primaryApp.exit() + secondaryApp.exit() + } + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt new file mode 100644 index 000000000000..5b8cfb81016a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt @@ -0,0 +1,47 @@ +/* + * 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.content.ComponentName +import android.graphics.Region +import com.android.server.wm.flicker.helpers.WindowUtils + +class AppPairsHelper( + instrumentation: Instrumentation, + activityLabel: String, + component: ComponentName +) : BaseAppHelper(instrumentation, activityLabel, component) { + fun getPrimaryBounds(dividerBounds: Region): android.graphics.Region { + val primaryAppBounds = Region(0, 0, dividerBounds.bounds.right, + dividerBounds.bounds.bottom + WindowUtils.dockedStackDividerInset) + return primaryAppBounds + } + + fun getSecondaryBounds(dividerBounds: Region): android.graphics.Region { + val displayBounds = WindowUtils.displayBounds + val secondaryAppBounds = Region(0, + dividerBounds.bounds.bottom - WindowUtils.dockedStackDividerInset, + displayBounds.right, displayBounds.bottom - WindowUtils.navigationBarHeight) + return secondaryAppBounds + } + + companion object { + const val TEST_REPETITIONS = 1 + const val TIMEOUT_MS = 3_000L + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt new file mode 100644 index 000000000000..006b569146e4 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt @@ -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.flicker.helpers + +import android.app.Instrumentation +import android.content.ComponentName +import android.content.pm.PackageManager.FEATURE_LEANBACK +import android.content.pm.PackageManager.FEATURE_LEANBACK_ONLY +import android.support.test.launcherhelper.LauncherStrategyFactory +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiObject2 +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.helpers.StandardAppHelper +import com.android.server.wm.traces.parser.toWindowName + +abstract class BaseAppHelper( + instrumentation: Instrumentation, + launcherName: String, + component: ComponentName +) : StandardAppHelper( + instrumentation, + launcherName, + component, + LauncherStrategyFactory.getInstance(instrumentation).launcherStrategy +) { + private val appSelector = By.pkg(component.packageName).depth(0) + + protected val isTelevision: Boolean + get() = context.packageManager.run { + hasSystemFeature(FEATURE_LEANBACK) || hasSystemFeature(FEATURE_LEANBACK_ONLY) + } + + val defaultWindowName: String + get() = component.toWindowName() + + val ui: UiObject2? + get() = uiDevice.findObject(appSelector) + + fun waitUntilClosed(): Boolean { + return uiDevice.wait(Until.gone(appSelector), APP_CLOSE_WAIT_TIME_MS) + } + + companion object { + private const val APP_CLOSE_WAIT_TIME_MS = 3_000L + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt new file mode 100644 index 000000000000..b4ae18749b34 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt @@ -0,0 +1,26 @@ +/* + * 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 com.android.wm.shell.flicker.testapp.Components + +class FixedAppHelper(instrumentation: Instrumentation) : BaseAppHelper( + instrumentation, + Components.FixedActivity.LABEL, + Components.FixedActivity.COMPONENT +)
\ No newline at end of file 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..7ec22bb9db1c --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.helpers + +import android.app.Instrumentation +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.traces.parser.windowmanager.WindowManagerStateHelper +import com.android.wm.shell.flicker.testapp.Components + +open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper( + instrumentation, + Components.ImeActivity.LABEL, + Components.ImeActivity.COMPONENT +) { + /** + * Opens the IME and wait for it to be displayed + * + * @param device UIDevice instance to interact with the device + * @param wmHelper Helper used to wait for WindowManager states + */ + @JvmOverloads + open fun openIME(device: UiDevice, wmHelper: WindowManagerStateHelper? = null) { + if (!isTelevision) { + val editText = device.wait( + Until.findObject(By.res(getPackage(), "plain_text_input")), + FIND_TIMEOUT) + + require(editText != null) { + "Text field not found, this usually happens when the device " + + "was left in an unknown state (e.g. in split screen)" + } + editText.click() + waitAndAssertIMEShown(device, wmHelper) + } else { + // If we do the same thing as above - editText.click() - on TV, that's going to force TV + // into the touch mode. We really don't want that. + launchViaIntent(action = Components.ImeActivity.ACTION_OPEN_IME) + } + } + + protected fun waitAndAssertIMEShown( + device: UiDevice, + wmHelper: WindowManagerStateHelper? = null + ) { + if (wmHelper == null) { + device.waitForIdle() + } else { + require(wmHelper.waitImeWindowShown()) { "IME did not appear" } + } + } + + /** + * Opens the IME and wait for it to be gone + * + * @param device UIDevice instance to interact with the device + * @param wmHelper Helper used to wait for WindowManager states + */ + @JvmOverloads + open fun closeIME(device: UiDevice, wmHelper: WindowManagerStateHelper? = null) { + if (!isTelevision) { + device.pressBack() + // Using only the AccessibilityInfo it is not possible to identify if the IME is active + if (wmHelper == null) { + device.waitForIdle() + } else { + require(wmHelper.waitImeWindowGone()) { "IME did did not close" } + } + } else { + // While pressing the back button should close the IME on TV as well, it may also lead + // to the app closing. So let's instead just ask the app to close the IME. + launchViaIntent(action = Components.ImeActivity.ACTION_CLOSE_IME) + } + } +}
\ 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..b90e865de691 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt @@ -0,0 +1,118 @@ +/* + * 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.media.session.MediaController +import android.media.session.MediaSessionManager +import android.os.SystemClock +import androidx.test.uiautomator.By +import androidx.test.uiautomator.BySelector +import com.android.server.wm.flicker.helpers.closePipWindow +import com.android.server.wm.flicker.helpers.hasPipWindow +import com.android.wm.shell.flicker.pip.tv.closeTvPipWindow +import com.android.wm.shell.flicker.pip.tv.isFocusedOrHasFocusedChild +import com.android.wm.shell.flicker.testapp.Components +import org.junit.Assert.fail + +class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( + instrumentation, + Components.PipActivity.LABEL, + Components.PipActivity.COMPONENT +) { + private val mediaSessionManager: MediaSessionManager + get() = context.getSystemService(MediaSessionManager::class.java) + ?: error("Could not get MediaSessionManager") + + private val mediaController: MediaController? + get() = mediaSessionManager.getActiveSessions(null).firstOrNull { + it.packageName == component.packageName + } + + fun clickObject(resId: String) { + val selector = By.res(component.packageName, resId) + val obj = uiDevice.findObject(selector) ?: error("Could not find `$resId` object") + + if (!isTelevision) { + obj.click() + } else { + focusOnObject(selector) || error("Could not focus on `$resId` object") + uiDevice.pressDPadCenter() + } + } + + private fun focusOnObject(selector: BySelector): Boolean { + // We expect all the focusable UI elements to be arranged in a way so that it is possible + // to "cycle" over all them by clicking the D-Pad DOWN button, going back up to "the top" + // from "the bottom". + repeat(FOCUS_ATTEMPTS) { + uiDevice.findObject(selector)?.apply { if (isFocusedOrHasFocusedChild) return true } + ?: error("The object we try to focus on is gone.") + + uiDevice.pressDPadDown() + uiDevice.waitForIdle() + } + return false + } + + fun clickEnterPipButton() { + clickObject(ENTER_PIP_BUTTON_ID) + + // TODO(b/172321238): remove this check once hasPipWindow is fixed on TVs + if (!isTelevision) { + uiDevice.hasPipWindow() + } else { + // Simply wait for 3 seconds + SystemClock.sleep(3_000) + } + } + + fun clickStartMediaSessionButton() { + clickObject(MEDIA_SESSION_START_RADIO_BUTTON_ID) + } + + fun checkWithCustomActionsCheckbox() = uiDevice + .findObject(By.res(component.packageName, WITH_CUSTOM_ACTIONS_BUTTON_ID)) + ?.takeIf { it.isCheckable } + ?.apply { if (!isChecked) clickObject(WITH_CUSTOM_ACTIONS_BUTTON_ID) } + ?: error("'With custom actions' checkbox not found") + + fun pauseMedia() = mediaController?.transportControls?.pause() + ?: error("No active media session found") + + fun stopMedia() = mediaController?.transportControls?.stop() + ?: error("No active media session found") + + fun closePipWindow() { + if (isTelevision) { + uiDevice.closeTvPipWindow() + } else { + uiDevice.closePipWindow() + } + + if (!waitUntilClosed()) { + fail("Couldn't close Pip") + } + } + + companion object { + private const val FOCUS_ATTEMPTS = 20 + private const val ENTER_PIP_BUTTON_ID = "enter_pip" + private const val WITH_CUSTOM_ACTIONS_BUTTON_ID = "with_custom_actions" + private const val MEDIA_SESSION_START_RADIO_BUTTON_ID = "media_session_start" + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt new file mode 100644 index 000000000000..ba13e38ae9e3 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.helpers + +import android.app.Instrumentation +import com.android.wm.shell.flicker.testapp.Components + +class SimpleAppHelper(instrumentation: Instrumentation) : BaseAppHelper( + instrumentation, + Components.SimpleActivity.LABEL, + Components.SimpleActivity.COMPONENT +)
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt new file mode 100644 index 000000000000..9f2087fc91d6 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt @@ -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.flicker.helpers + +import android.app.Instrumentation +import android.content.ComponentName +import android.os.SystemClock +import com.android.wm.shell.flicker.testapp.Components + +class SplitScreenHelper( + instrumentation: Instrumentation, + activityLabel: String, + componentsInfo: ComponentName +) : BaseAppHelper(instrumentation, activityLabel, componentsInfo) { + + /** + * Reopens the first device window from the list of recent apps (overview) + */ + fun reopenAppFromOverview() { + val x = uiDevice.displayWidth / 2 + val y = uiDevice.displayHeight / 2 + uiDevice.click(x, y) + // Wait for animation to complete. + SystemClock.sleep(TIMEOUT_MS) + } + + companion object { + const val TEST_REPETITIONS = 1 + const val TIMEOUT_MS = 3_000L + + fun getPrimary(instrumentation: Instrumentation): SplitScreenHelper = + SplitScreenHelper(instrumentation, + Components.SplitScreenActivity.LABEL, + Components.SplitScreenActivity.COMPONENT) + + fun getSecondary(instrumentation: Instrumentation): SplitScreenHelper = + SplitScreenHelper(instrumentation, + Components.SplitScreenSecondaryActivity.LABEL, + Components.SplitScreenSecondaryActivity.COMPONENT) + + fun getNonResizeable(instrumentation: Instrumentation): SplitScreenHelper = + SplitScreenHelper(instrumentation, + Components.NonResizeableActivity.LABEL, + Components.NonResizeableActivity.COMPONENT) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt new file mode 100644 index 000000000000..5374bd9f40de --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.WALLPAPER_TITLE +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry +import com.android.server.wm.flicker.visibleWindowsShownMoreThanOneConsecutiveEntry +import com.android.wm.shell.flicker.dockedStackDividerBecomesVisible +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test open activity and dock to primary split screen + * To run this test: `atest WMShellFlickerTests:EnterSplitScreenDockActivity` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class EnterSplitScreenDockActivity( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testLegacySplitScreenDockActivity", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + device.launchSplitScreen() + } + assertions { + layersTrace { + dockedStackPrimaryBoundsIsVisible( + configuration.startRotation, + splitScreenApp.defaultWindowName, bugId = 169271943) + dockedStackDividerBecomesVisible() + visibleLayersShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, + WALLPAPER_TITLE, LIVE_WALLPAPER_PACKAGE_NAME, + splitScreenApp.defaultWindowName), + bugId = 178531736 + ) + } + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + visibleWindowsShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, + WALLPAPER_TITLE, LIVE_WALLPAPER_PACKAGE_NAME, + splitScreenApp.defaultWindowName), + bugId = 178531736 + ) + end("appWindowIsVisible") { + isVisible(splitScreenApp.defaultWindowName) + } + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, defaultTransitionSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt new file mode 100644 index 000000000000..d750403d66c6 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.appWindowBecomesVisible +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry +import com.android.server.wm.flicker.visibleWindowsShownMoreThanOneConsecutiveEntry +import com.android.wm.shell.flicker.dockedStackDividerBecomesVisible +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible +import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test open activity to primary split screen and dock secondary activity to side + * To run this test: `atest WMShellFlickerTests:EnterSplitScreenLaunchToSide` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class EnterSplitScreenLaunchToSide( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testLegacySplitScreenLaunchToSide", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + device.launchSplitScreen() + secondaryApp.reopenAppFromOverview() + } + assertions { + layersTrace { + dockedStackPrimaryBoundsIsVisible( + configuration.startRotation, + splitScreenApp.defaultWindowName, bugId = 169271943) + dockedStackSecondaryBoundsIsVisible( + configuration.startRotation, + secondaryApp.defaultWindowName, bugId = 169271943) + dockedStackDividerBecomesVisible() + // TODO(b/178447631) Remove Splash Screen from white list when flicker lib + // add a wait for splash screen be gone + visibleLayersShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, SPLASH_SCREEN_NAME, + splitScreenApp.defaultWindowName, + secondaryApp.defaultWindowName), + bugId = 178447631 + ) + } + windowManagerTrace { + appWindowBecomesVisible(secondaryApp.defaultWindowName) + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + visibleWindowsShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, SPLASH_SCREEN_NAME, + splitScreenApp.defaultWindowName, + secondaryApp.defaultWindowName) + ) + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, defaultTransitionSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNonResizableNotDock.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNonResizableNotDock.kt new file mode 100644 index 000000000000..e3619235ee77 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNonResizableNotDock.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.FlakyTest +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.WALLPAPER_TITLE +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.canSplitScreen +import com.android.server.wm.flicker.helpers.openQuickstep +import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry +import com.android.server.wm.flicker.visibleWindowsShownMoreThanOneConsecutiveEntry +import com.android.wm.shell.flicker.dockedStackDividerIsInvisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.Assert +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test open non-resizable activity will auto exit split screen mode + * To run this test: `atest WMShellFlickerTests:EnterSplitScreenNonResizableNotDock` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@FlakyTest(bugId = 173875043) +class EnterSplitScreenNonResizableNotDock( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testLegacySplitScreenNonResizeableActivityNotDock", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + nonResizeableApp.launchViaIntent(wmHelper) + device.openQuickstep() + if (device.canSplitScreen()) { + Assert.fail("Non-resizeable app should not enter split screen") + } + } + assertions { + layersTrace { + dockedStackDividerIsInvisible() + visibleLayersShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, + SPLASH_SCREEN_NAME, + nonResizeableApp.defaultWindowName, + splitScreenApp.defaultWindowName), + bugId = 178447631 + ) + } + windowManagerTrace { + visibleWindowsShownMoreThanOneConsecutiveEntry( + listOf(WALLPAPER_TITLE, + LAUNCHER_PACKAGE_NAME, + SPLASH_SCREEN_NAME, + nonResizeableApp.defaultWindowName, + splitScreenApp.defaultWindowName) + ) + end("appWindowIsVisible") { + isInvisible(nonResizeableApp.defaultWindowName) + } + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, defaultTransitionSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0 /* bugId = 178685668 */)) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt new file mode 100644 index 000000000000..7782364636f1 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.DOCKED_STACK_DIVIDER +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.appWindowBecomesInVisible +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.exitSplitScreenFromBottom +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.layerBecomesInvisible +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry +import com.android.server.wm.flicker.visibleWindowsShownMoreThanOneConsecutiveEntry +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test open resizeable activity split in primary, and drag divider to bottom exit split screen + * To run this test: `atest WMShellFlickerTests:ExitLegacySplitScreenFromBottom` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ExitLegacySplitScreenFromBottom( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testExitLegacySplitScreenFromBottom", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + device.launchSplitScreen() + device.exitSplitScreenFromBottom() + } + assertions { + layersTrace { + layerBecomesInvisible(DOCKED_STACK_DIVIDER) + visibleLayersShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName, + secondaryApp.defaultWindowName), + bugId = 178447631 + ) + } + windowManagerTrace { + appWindowBecomesInVisible(secondaryApp.defaultWindowName) + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + visibleWindowsShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName, + secondaryApp.defaultWindowName), + bugId = 178447631 + ) + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, defaultTransitionSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt new file mode 100644 index 000000000000..59f6aaf7dd6a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.legacysplitscreen + +import android.os.Bundle +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.appWindowBecomesInVisible +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.layerBecomesInvisible +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry +import com.android.server.wm.flicker.visibleWindowsShownMoreThanOneConsecutiveEntry +import com.android.wm.shell.flicker.dockedStackDividerIsInvisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test dock activity to primary split screen, and open secondary to side, exit primary split + * and test secondary activity become full screen. + * To run this test: `atest WMShellFlickerTests:ExitPrimarySplitScreenShowSecondaryFullscreen` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ExitPrimarySplitScreenShowSecondaryFullscreen( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testExitPrimarySplitScreenShowSecondaryFullscreen", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + device.launchSplitScreen() + secondaryApp.reopenAppFromOverview() + // TODO(b/175687842) Can not find Split screen divider, use exit() instead + splitScreenApp.exit() + } + assertions { + layersTrace { + dockedStackDividerIsInvisible(bugId = 175687842) + layerBecomesInvisible(splitScreenApp.defaultWindowName) + visibleLayersShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName, + secondaryApp.defaultWindowName), + bugId = 178447631 + ) + } + windowManagerTrace { + appWindowBecomesInVisible(splitScreenApp.defaultWindowName) + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + visibleWindowsShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName, + secondaryApp.defaultWindowName), + bugId = 178447631 + ) + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, defaultTransitionSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt new file mode 100644 index 000000000000..03b6edf0ff2a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt @@ -0,0 +1,134 @@ +/* + * 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.legacysplitscreen + +import android.platform.test.annotations.Presubmit +import android.support.test.launcherhelper.LauncherStrategyFactory +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.focusDoesNotChange +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.exitSplitScreen +import com.android.server.wm.flicker.helpers.isInSplitScreen +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.helpers.openQuickStepAndClearRecentAppsFromOverview +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.visibleWindowsShownMoreThanOneConsecutiveEntry +import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry +import com.android.server.wm.flicker.layerBecomesInvisible +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.noUncoveredRegions +import com.android.server.wm.flicker.repetitions +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.dockedStackDividerBecomesInvisible +import com.android.wm.shell.flicker.helpers.SimpleAppHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test open app to split screen. + * To run this test: `atest WMShellFlickerTests:LegacySplitScreenToLauncher` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class LegacySplitScreenToLauncher( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val launcherPackageName = LauncherStrategyFactory.getInstance(instrumentation) + .launcherStrategy.supportedLauncherPackage + val testApp = SimpleAppHelper(instrumentation) + + // b/161435597 causes the test not to work on 90 degrees + return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, + supportedRotations = listOf(Surface.ROTATION_0)) { configuration -> + withTestName { + buildTestTag("splitScreenToLauncher", configuration) + } + repeat { configuration.repetitions } + setup { + test { + device.wakeUpAndGoToHomeScreen() + device.openQuickStepAndClearRecentAppsFromOverview() + } + eachRun { + testApp.launchViaIntent(wmHelper) + this.setRotation(configuration.endRotation) + device.launchSplitScreen() + device.waitForIdle() + } + } + teardown { + eachRun { + testApp.exit() + } + test { + if (device.isInSplitScreen()) { + device.exitSplitScreen() + } + } + } + transitions { + device.exitSplitScreen() + } + assertions { + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + visibleWindowsShownMoreThanOneConsecutiveEntry() + } + + layersTrace { + navBarLayerIsAlwaysVisible() + statusBarLayerIsAlwaysVisible() + noUncoveredRegions(configuration.endRotation) + navBarLayerRotatesAndScales(configuration.endRotation) + statusBarLayerRotatesScales(configuration.endRotation) + visibleLayersShownMoreThanOneConsecutiveEntry( + listOf(launcherPackageName)) + + // b/161435597 causes the test not to work on 90 degrees + dockedStackDividerBecomesInvisible() + + layerBecomesInvisible(testApp.getPackage()) + } + + eventLog { + focusDoesNotChange(bugId = 151179149) + } + } + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt new file mode 100644 index 000000000000..328ff88cd41b --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt @@ -0,0 +1,99 @@ +/* + * 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/LICENSE2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.legacysplitscreen + +import android.app.Instrumentation +import android.os.Bundle +import android.support.test.launcherhelper.LauncherStrategyFactory +import android.view.Surface +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.openQuickStepAndClearRecentAppsFromOverview +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.startRotation +import com.android.wm.shell.flicker.helpers.SplitScreenHelper + +abstract class LegacySplitScreenTransition( + protected val instrumentation: Instrumentation +) { + internal val splitScreenApp = SplitScreenHelper.getPrimary(instrumentation) + internal val secondaryApp = SplitScreenHelper.getSecondary(instrumentation) + internal val nonResizeableApp = SplitScreenHelper.getNonResizeable(instrumentation) + internal val LAUNCHER_PACKAGE_NAME = LauncherStrategyFactory.getInstance(instrumentation) + .launcherStrategy.supportedLauncherPackage + internal val LIVE_WALLPAPER_PACKAGE_NAME = + "com.breel.wallpapers18.soundviz.wallpaper.variations.SoundVizWallpaperV2" + internal val LETTERBOX_NAME = "Letterbox" + internal val TOAST_NAME = "Toast" + internal val SPLASH_SCREEN_NAME = "Splash Screen" + + internal open val defaultTransitionSetup: FlickerBuilder.(Bundle) -> Unit + get() = { configuration -> + setup { + eachRun { + device.wakeUpAndGoToHomeScreen() + device.openQuickStepAndClearRecentAppsFromOverview() + secondaryApp.launchViaIntent(wmHelper) + splitScreenApp.launchViaIntent(wmHelper) + this.setRotation(configuration.startRotation) + } + } + teardown { + eachRun { + splitScreenApp.exit() + secondaryApp.exit() + this.setRotation(Surface.ROTATION_0) + } + } + } + + internal open val cleanSetup: FlickerBuilder.(Bundle) -> Unit + get() = { configuration -> + setup { + eachRun { + device.wakeUpAndGoToHomeScreen() + device.openQuickStepAndClearRecentAppsFromOverview() + this.setRotation(configuration.startRotation) + } + } + teardown { + eachRun { + nonResizeableApp.exit() + this.setRotation(Surface.ROTATION_0) + } + } + } + + internal open val customRotateSetup: FlickerBuilder.(Bundle) -> Unit + get() = { configuration -> + setup { + eachRun { + device.wakeUpAndGoToHomeScreen() + device.openQuickStepAndClearRecentAppsFromOverview() + secondaryApp.launchViaIntent(wmHelper) + splitScreenApp.launchViaIntent(wmHelper) + } + } + teardown { + eachRun { + splitScreenApp.exit() + secondaryApp.exit() + this.setRotation(Surface.ROTATION_0) + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/NonResizableDismissInLegacySplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/NonResizableDismissInLegacySplitScreen.kt new file mode 100644 index 000000000000..f2a7cda3b42d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/NonResizableDismissInLegacySplitScreen.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.DOCKED_STACK_DIVIDER +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.appWindowBecomesInVisible +import com.android.server.wm.flicker.appWindowBecomesVisible +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.layerBecomesInvisible +import com.android.server.wm.flicker.layerBecomesVisible +import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry +import com.android.server.wm.flicker.visibleWindowsShownMoreThanOneConsecutiveEntry +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test launch non resizable activity in split screen mode will trigger exit split screen mode + * (Non resizable activity launch via recent overview) + * To run this test: `atest WMShellFlickerTests:NonResizableDismissInLegacySplitScreen` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class NonResizableDismissInLegacySplitScreen( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testNonResizableDismissInLegacySplitScreen", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + nonResizeableApp.launchViaIntent(wmHelper) + splitScreenApp.launchViaIntent(wmHelper) + device.launchSplitScreen() + nonResizeableApp.reopenAppFromOverview() + wmHelper.waitForAppTransitionIdle() + } + assertions { + layersTrace { + layerBecomesVisible(nonResizeableApp.defaultWindowName) + layerBecomesInvisible(splitScreenApp.defaultWindowName) + visibleLayersShownMoreThanOneConsecutiveEntry( + listOf(DOCKED_STACK_DIVIDER, LAUNCHER_PACKAGE_NAME, + LETTERBOX_NAME, TOAST_NAME, + splitScreenApp.defaultWindowName, + nonResizeableApp.defaultWindowName), + bugId = 178447631 + ) + } + windowManagerTrace { + appWindowBecomesVisible(nonResizeableApp.defaultWindowName) + appWindowBecomesInVisible(splitScreenApp.defaultWindowName) + visibleWindowsShownMoreThanOneConsecutiveEntry( + listOf(DOCKED_STACK_DIVIDER, LAUNCHER_PACKAGE_NAME, + LETTERBOX_NAME, TOAST_NAME, + splitScreenApp.defaultWindowName, + nonResizeableApp.defaultWindowName), + bugId = 178447631 + ) + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, cleanSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0 /* bugId = 178685668 */)) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/NonResizableLaunchInLegacySplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/NonResizableLaunchInLegacySplitScreen.kt new file mode 100644 index 000000000000..421ecffc97d8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/NonResizableLaunchInLegacySplitScreen.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.DOCKED_STACK_DIVIDER +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.appWindowBecomesInVisible +import com.android.server.wm.flicker.appWindowBecomesVisible +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.layerBecomesInvisible +import com.android.server.wm.flicker.layerBecomesVisible +import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry +import com.android.server.wm.flicker.visibleWindowsShownMoreThanOneConsecutiveEntry +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test launch non resizable activity in split screen mode will trigger exit split screen mode + * (Non resizable activity launch via intent) + * To run this test: `atest WMShellFlickerTests:NonResizableLaunchInLegacySplitScreen` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class NonResizableLaunchInLegacySplitScreen( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testNonResizableLaunchInLegacySplitScreen", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + splitScreenApp.launchViaIntent(wmHelper) + device.launchSplitScreen() + nonResizeableApp.launchViaIntent(wmHelper) + wmHelper.waitForAppTransitionIdle() + } + assertions { + layersTrace { + layerBecomesVisible(nonResizeableApp.defaultWindowName) + layerBecomesInvisible(splitScreenApp.defaultWindowName) + visibleLayersShownMoreThanOneConsecutiveEntry( + listOf(DOCKED_STACK_DIVIDER, + LAUNCHER_PACKAGE_NAME, + LETTERBOX_NAME, + nonResizeableApp.defaultWindowName, + splitScreenApp.defaultWindowName), + bugId = 178447631 + ) + } + windowManagerTrace { + appWindowBecomesVisible(nonResizeableApp.defaultWindowName) + appWindowBecomesInVisible(splitScreenApp.defaultWindowName) + visibleWindowsShownMoreThanOneConsecutiveEntry( + listOf(DOCKED_STACK_DIVIDER, + LAUNCHER_PACKAGE_NAME, + LETTERBOX_NAME, + nonResizeableApp.defaultWindowName, + splitScreenApp.defaultWindowName), + bugId = 178447631 + ) + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, cleanSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0 /* bugId = 178685668 */)) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt new file mode 100644 index 000000000000..c802ffef204f --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt @@ -0,0 +1,99 @@ +/* + * 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.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.appWindowBecomesVisible +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.focusChanges +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.layerBecomesVisible +import com.android.server.wm.flicker.noUncoveredRegions +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry +import com.android.server.wm.flicker.visibleWindowsShownMoreThanOneConsecutiveEntry +import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.android.wm.shell.flicker.appPairsDividerBecomesVisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test open app to split screen. + * To run this test: `atest WMShellFlickerTests:OpenAppToLegacySplitScreen` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class OpenAppToLegacySplitScreen( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val wmHelper = WindowManagerStateHelper() + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testOpenAppToLegacySplitScreen", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + device.launchSplitScreen() + wmHelper.waitForAppTransitionIdle() + } + assertions { + windowManagerTrace { + visibleWindowsShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName), + bugId = 178447631) + appWindowBecomesVisible(splitScreenApp.getPackage()) + } + + layersTrace { + noUncoveredRegions(configuration.startRotation, enabled = false) + statusBarLayerIsAlwaysVisible() + appPairsDividerBecomesVisible() + layerBecomesVisible(splitScreenApp.getPackage()) + visibleLayersShownMoreThanOneConsecutiveEntry( + listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName), + bugId = 178447631) + } + + eventLog { + focusChanges(splitScreenApp.`package`, + "recents_animation_input_consumer", "NexusLauncherActivity", + bugId = 151179149) + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, defaultTransitionSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt new file mode 100644 index 000000000000..54a37d71868d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt @@ -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.flicker.legacysplitscreen + +import android.platform.test.annotations.Presubmit +import android.graphics.Region +import android.util.Rational +import android.view.Surface +import androidx.test.filters.FlakyTest +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.By +import com.android.server.wm.flicker.DOCKED_STACK_DIVIDER +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.server.wm.flicker.focusDoesNotChange +import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.exitSplitScreen +import com.android.server.wm.flicker.helpers.isInSplitScreen +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.helpers.resizeSplitScreen +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.noUncoveredRegions +import com.android.server.wm.flicker.repetitions +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.visibleWindowsShownMoreThanOneConsecutiveEntry +import com.android.server.wm.flicker.visibleLayersShownMoreThanOneConsecutiveEntry +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.traces.layers.getVisibleBounds +import com.android.wm.shell.flicker.helpers.SimpleAppHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test split screen resizing window transitions. + * To run this test: `atest WMShellFlickerTests:ResizeLegacySplitScreen` + * + * Currently it runs only in 0 degrees because of b/156100803 + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@FlakyTest(bugId = 159096424) +class ResizeLegacySplitScreen( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object { + private const val sSimpleActivity = "SimpleActivity" + private const val sImeActivity = "ImeActivity" + private val startRatio = Rational(1, 3) + private val stopRatio = Rational(2, 3) + + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val testAppTop = SimpleAppHelper(instrumentation) + val testAppBottom = ImeAppHelper(instrumentation) + + return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, + supportedRotations = listOf(Surface.ROTATION_0)) { configuration -> + withTestName { + val description = (startRatio.toString().replace("/", "-") + "_to_" + + stopRatio.toString().replace("/", "-")) + buildTestTag("resizeSplitScreen", configuration, description) + } + repeat { configuration.repetitions } + setup { + eachRun { + device.wakeUpAndGoToHomeScreen() + this.setRotation(configuration.startRotation) + this.launcherStrategy.clearRecentAppsFromOverview() + testAppBottom.launchViaIntent(wmHelper) + device.pressHome() + testAppTop.launchViaIntent(wmHelper) + device.waitForIdle() + device.launchSplitScreen() + val snapshot = + device.findObject(By.res(device.launcherPackageName, "snapshot")) + snapshot.click() + testAppBottom.openIME(device) + device.pressBack() + device.resizeSplitScreen(startRatio) + } + } + teardown { + eachRun { + if (device.isInSplitScreen()) { + device.exitSplitScreen() + } + device.pressHome() + testAppTop.exit() + testAppBottom.exit() + } + test { + if (device.isInSplitScreen()) { + device.exitSplitScreen() + } + } + } + transitions { + device.resizeSplitScreen(stopRatio) + } + assertions { + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + visibleWindowsShownMoreThanOneConsecutiveEntry() + + all("topAppWindowIsAlwaysVisible", bugId = 156223549) { + this.showsAppWindow(sSimpleActivity) + } + + all("bottomAppWindowIsAlwaysVisible", bugId = 156223549) { + this.showsAppWindow(sImeActivity) + } + } + + layersTrace { + navBarLayerIsAlwaysVisible() + statusBarLayerIsAlwaysVisible() + noUncoveredRegions(configuration.endRotation) + navBarLayerRotatesAndScales(configuration.endRotation) + statusBarLayerRotatesScales(configuration.endRotation) + visibleLayersShownMoreThanOneConsecutiveEntry() + + all("topAppLayerIsAlwaysVisible") { + this.showsLayer(sSimpleActivity) + } + + all("bottomAppLayerIsAlwaysVisible") { + this.showsLayer(sImeActivity) + } + + all("dividerLayerIsAlwaysVisible") { + this.showsLayer(DOCKED_STACK_DIVIDER) + } + + start("appsStartingBounds", enabled = false) { + val displayBounds = WindowUtils.displayBounds + val dividerBounds = + entry.getVisibleBounds(DOCKED_STACK_DIVIDER).bounds + + val topAppBounds = Region(0, 0, dividerBounds.right, + dividerBounds.top + WindowUtils.dockedStackDividerInset) + val bottomAppBounds = Region(0, + dividerBounds.bottom - WindowUtils.dockedStackDividerInset, + displayBounds.right, + displayBounds.bottom - WindowUtils.navigationBarHeight) + this.hasVisibleRegion("SimpleActivity", topAppBounds) + .hasVisibleRegion("ImeActivity", bottomAppBounds) + } + + end("appsEndingBounds", enabled = false) { + val displayBounds = WindowUtils.displayBounds + val dividerBounds = + entry.getVisibleBounds(DOCKED_STACK_DIVIDER).bounds + + val topAppBounds = Region(0, 0, dividerBounds.right, + dividerBounds.top + WindowUtils.dockedStackDividerInset) + val bottomAppBounds = Region(0, + dividerBounds.bottom - WindowUtils.dockedStackDividerInset, + displayBounds.right, + displayBounds.bottom - WindowUtils.navigationBarHeight) + + this.hasVisibleRegion(sSimpleActivity, topAppBounds) + .hasVisibleRegion(sImeActivity, bottomAppBounds) + } + } + + eventLog { + focusDoesNotChange() + } + } + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt new file mode 100644 index 000000000000..214269e13203 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt @@ -0,0 +1,95 @@ +/* + * 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.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.appWindowBecomesVisible +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.dockedStackDividerIsVisible +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test dock activity to primary split screen and rotate + * To run this test: `atest WMShellFlickerTests:RotateOneLaunchedAppAndEnterSplitScreen` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class RotateOneLaunchedAppAndEnterSplitScreen( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testRotateOneLaunchedAppAndEnterSplitScreen", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + device.launchSplitScreen() + this.setRotation(configuration.startRotation) + } + assertions { + layersTrace { + dockedStackDividerIsVisible(bugId = 175687842) + dockedStackPrimaryBoundsIsVisible( + configuration.startRotation, + splitScreenApp.defaultWindowName, bugId = 175687842) + navBarLayerRotatesAndScales( + configuration.startRotation, + configuration.endRotation, bugId = 169271943) + statusBarLayerRotatesScales( + configuration.startRotation, + configuration.endRotation, bugId = 169271943) + } + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + appWindowBecomesVisible(splitScreenApp.defaultWindowName) + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, customRotateSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0 /* bugId = 178685668 */)) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt new file mode 100644 index 000000000000..4290c923b38d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt @@ -0,0 +1,95 @@ +/* + * 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.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.appWindowBecomesVisible +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.dockedStackDividerIsVisible +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Rotate + * To run this test: `atest WMShellFlickerTests:RotateOneLaunchedAppInSplitScreenMode` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class RotateOneLaunchedAppInSplitScreenMode( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testRotateOneLaunchedAppInSplitScreenMode", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + this.setRotation(configuration.startRotation) + device.launchSplitScreen() + } + assertions { + layersTrace { + dockedStackDividerIsVisible(bugId = 175687842) + dockedStackPrimaryBoundsIsVisible( + configuration.startRotation, + splitScreenApp.defaultWindowName, bugId = 175687842) + navBarLayerRotatesAndScales( + configuration.startRotation, + configuration.endRotation, bugId = 169271943) + statusBarLayerRotatesScales( + configuration.startRotation, + configuration.endRotation, bugId = 169271943) + } + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + appWindowBecomesVisible(splitScreenApp.defaultWindowName) + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, customRotateSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0 /* bugId = 178685668 */)) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt new file mode 100644 index 000000000000..4095b9a2e61e --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt @@ -0,0 +1,100 @@ +/* + * 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.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.appWindowBecomesVisible +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.dockedStackDividerIsVisible +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible +import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test open app to split screen. + * To run this test: `atest WMShellFlickerTests:RotateTwoLaunchedAppAndEnterSplitScreen` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class RotateTwoLaunchedAppAndEnterSplitScreen( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testRotateTwoLaunchedAppAndEnterSplitScreen", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + transitions { + this.setRotation(configuration.startRotation) + device.launchSplitScreen() + secondaryApp.reopenAppFromOverview() + } + assertions { + layersTrace { + dockedStackDividerIsVisible(bugId = 175687842) + dockedStackPrimaryBoundsIsVisible( + configuration.startRotation, + splitScreenApp.defaultWindowName, 175687842) + dockedStackSecondaryBoundsIsVisible( + configuration.startRotation, + secondaryApp.defaultWindowName, bugId = 175687842) + navBarLayerRotatesAndScales( + configuration.startRotation, + configuration.endRotation, bugId = 169271943) + statusBarLayerRotatesScales( + configuration.startRotation, + configuration.endRotation, bugId = 169271943) + } + windowManagerTrace { + appWindowBecomesVisible(secondaryApp.defaultWindowName) + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, customRotateSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0 /* bugId = 178685668 */)) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt new file mode 100644 index 000000000000..aebf6067615e --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt @@ -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.flicker.legacysplitscreen + +import android.os.Bundle +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.appWindowBecomesVisible +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.dockedStackDividerIsVisible +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible +import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test open app to split screen. + * To run this test: `atest WMShellFlickerTests:RotateTwoLaunchedAppInSplitScreenMode` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class RotateTwoLaunchedAppInSplitScreenMode( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object : LegacySplitScreenTransition(InstrumentationRegistry.getInstrumentation()) { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val testSpec: FlickerBuilder.(Bundle) -> Unit = { configuration -> + withTestName { + buildTestTag("testRotateTwoLaunchedAppInSplitScreenMode", configuration) + } + repeat { SplitScreenHelper.TEST_REPETITIONS } + setup { + eachRun { + device.launchSplitScreen() + splitScreenApp.reopenAppFromOverview() + this.setRotation(configuration.startRotation) + } + } + transitions { + this.setRotation(configuration.startRotation) + } + assertions { + layersTrace { + dockedStackDividerIsVisible(bugId = 175687842) + dockedStackPrimaryBoundsIsVisible( + configuration.startRotation, + splitScreenApp.defaultWindowName, bugId = 175687842) + dockedStackSecondaryBoundsIsVisible( + configuration.startRotation, + secondaryApp.defaultWindowName, bugId = 175687842) + navBarLayerRotatesAndScales( + configuration.startRotation, + configuration.endRotation, bugId = 169271943) + statusBarLayerRotatesScales( + configuration.startRotation, + configuration.endRotation, bugId = 169271943) + } + windowManagerTrace { + appWindowBecomesVisible(secondaryApp.defaultWindowName) + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + } + } + } + return FlickerTestRunnerFactory.getInstance().buildTest( + instrumentation, customRotateSetup, testSpec, + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0 /* bugId = 178685668 */)) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AppTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AppTestBase.kt new file mode 100644 index 000000000000..2015f4941cea --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AppTestBase.kt @@ -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. + */ + +package com.android.wm.shell.flicker.pip + +import android.app.ActivityTaskManager +import android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT +import android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED +import android.os.SystemClock +import com.android.wm.shell.flicker.NonRotationTestBase + +abstract class AppTestBase( + rotationName: String, + rotation: Int +) : NonRotationTestBase(rotationName, rotation) { + companion object { + fun removeAllTasksButHome() { + val ALL_ACTIVITY_TYPE_BUT_HOME = intArrayOf( + ACTIVITY_TYPE_STANDARD, ACTIVITY_TYPE_ASSISTANT, ACTIVITY_TYPE_RECENTS, + ACTIVITY_TYPE_UNDEFINED) + val atm = ActivityTaskManager.getService() + atm.removeRootTasksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME) + } + + fun waitForAnimationComplete() { + // TODO: UiDevice doesn't have reliable way to wait for the completion of animation. + // Consider to introduce WindowManagerStateHelper to access Activity state. + SystemClock.sleep(1000) + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/WindowManagerShell.java b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt index 273bd27a221b..2a660747bc1d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/WindowManagerShell.java +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * 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. @@ -14,10 +14,6 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.flicker.pip -/** - * Interface for the shell. - */ -public class WindowManagerShell { -} +internal const val PIP_WINDOW_TITLE = "PipMenuActivity" diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt new file mode 100644 index 000000000000..5a3d18d9feef --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt @@ -0,0 +1,120 @@ +/* + * 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.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.dsl.runFlicker +import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.wm.shell.flicker.helpers.FixedAppHelper +import com.android.wm.shell.flicker.helpers.PipAppHelper +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP +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 and exit. + * To run this test: `atest WMShellFlickerTests:EnterExitPipTest` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class EnterExitPipTest( + rotationName: String, + rotation: Int +) : AppTestBase(rotationName, rotation) { + private val pipApp = PipAppHelper(instrumentation) + private val testApp = FixedAppHelper(instrumentation) + + @Test + fun testDisplayMetricsPinUnpin() { + runFlicker(instrumentation) { + withTestName { "testDisplayMetricsPinUnpin" } + setup { + test { + removeAllTasksButHome() + device.wakeUpAndGoToHomeScreen() + pipApp.launchViaIntent(stringExtras = mapOf(EXTRA_ENTER_PIP to "true")) + testApp.launchViaIntent() + waitForAnimationComplete() + } + } + transitions { + // This will bring PipApp to fullscreen + pipApp.launchViaIntent() + waitForAnimationComplete() + } + teardown { + test { + removeAllTasksButHome() + } + } + assertions { + val displayBounds = WindowUtils.getDisplayBounds(rotation) + windowManagerTrace { + all("pipApp must remain inside visible bounds") { + coversAtMostRegion(pipApp.defaultWindowName, displayBounds) + } + all("Initially shows both app windows then pipApp hides testApp") { + showsAppWindow(testApp.defaultWindowName) + .showsAppWindowOnTop(pipApp.defaultWindowName) + .then() + .hidesAppWindow(testApp.defaultWindowName) + } + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + } + layersTrace { + all("Initially shows both app layers then pipApp hides testApp") { + showsLayer(testApp.defaultWindowName) + .showsLayer(pipApp.defaultWindowName) + .then() + .hidesLayer(testApp.defaultWindowName) + } + start("testApp covers the fullscreen, pipApp remains inside display") { + hasVisibleRegion(testApp.defaultWindowName, displayBounds) + coversAtMostRegion(displayBounds, pipApp.defaultWindowName) + } + end("pipApp covers the fullscreen") { + hasVisibleRegion(pipApp.defaultWindowName, displayBounds) + } + navBarLayerIsAlwaysVisible() + statusBarLayerIsAlwaysVisible() + } + } + } + } + + 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/EnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt new file mode 100644 index 000000000000..af62eb9ae40d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt @@ -0,0 +1,129 @@ +/* + * 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 androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.helpers.buildTestTag +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.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.noUncoveredRegions +import com.android.server.wm.flicker.repetitions +import com.android.server.wm.flicker.startRotation +import com.android.wm.shell.flicker.helpers.PipAppHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test Pip launch. + * To run this test: `atest WMShellFlickerTests:EnterPipTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@FlakyTest(bugId = 152738416) +class EnterPipTest( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<Array<Any>> { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val testApp = PipAppHelper(instrumentation) + return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, + supportedRotations = listOf(Surface.ROTATION_0)) { configuration -> + withTestName { buildTestTag("enterPip", testApp, configuration) } + repeat { configuration.repetitions } + setup { + test { + device.wakeUpAndGoToHomeScreen() + } + eachRun { + device.pressHome() + testApp.launchViaIntent(wmHelper) + this.setRotation(configuration.startRotation) + } + } + teardown { + eachRun { + if (device.hasPipWindow()) { + device.closePipWindow() + } + testApp.exit() + this.setRotation(Surface.ROTATION_0) + } + test { + if (device.hasPipWindow()) { + device.closePipWindow() + } + } + } + transitions { + testApp.clickEnterPipButton() + device.expandPipWindow() + } + assertions { + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + + all("pipWindowBecomesVisible") { + this.showsAppWindow(testApp.`package`) + .then() + .showsAppWindow(PIP_WINDOW_TITLE) + } + } + + layersTrace { + navBarLayerIsAlwaysVisible(bugId = 140855415) + statusBarLayerIsAlwaysVisible() + noUncoveredRegions(configuration.startRotation, Surface.ROTATION_0, + enabled = false) + navBarLayerRotatesAndScales(configuration.startRotation, + Surface.ROTATION_0, bugId = 140855415) + statusBarLayerRotatesScales(configuration.startRotation, + Surface.ROTATION_0) + } + + layersTrace { + all("pipLayerBecomesVisible") { + this.showsLayer(testApp.launcherName) + .then() + .showsLayer(PIP_WINDOW_TITLE) + } + } + } + } + } + } +}
\ No newline at end of file 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..c21b594246b9 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt @@ -0,0 +1,152 @@ +/* + * 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.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.dsl.runWithFlicker +import com.android.server.wm.flicker.helpers.WindowUtils +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.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.android.wm.shell.flicker.IME_WINDOW_NAME +import com.android.wm.shell.flicker.helpers.ImeAppHelper +import com.android.wm.shell.flicker.testapp.Components +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:PipKeyboardTest` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class PipKeyboardTest( + rotationName: String, + rotation: Int +) : PipTestBase(rotationName, rotation) { + private val keyboardApp = ImeAppHelper(instrumentation) + private val keyboardComponent = Components.ImeActivity.COMPONENT + private val helper = WindowManagerStateHelper() + + private val keyboardScenario: FlickerBuilder + get() = FlickerBuilder(instrumentation).apply { + repeat { TEST_REPETITIONS } + // disable layer tracing + withLayerTracing { null } + setup { + test { + device.wakeUpAndGoToHomeScreen() + device.pressHome() + // launch our target pip app + testApp.launchViaIntent(wmHelper) + this.setRotation(rotation) + testApp.clickEnterPipButton() + // 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. + keyboardApp.launchViaIntent() + helper.waitForAppTransitionIdle() + helper.waitForFullScreenApp(keyboardComponent) + } + } + 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, wmHelper) + helper.waitImeWindowShown() + + // then close it again + keyboardApp.closeIME(device, wmHelper) + helper.waitImeWindowGone() + } + assertions { + windowManagerTrace { + all("PiP window must remain inside visible bounds") { + val displayBounds = WindowUtils.getDisplayBounds(rotation) + coversAtMostRegion(testApp.defaultWindowName, displayBounds) + } + } + } + } + } + + /** 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, wmHelper) + helper.waitImeWindowShown() + } + teardown { + eachRun { + // close the keyboard + keyboardApp.closeIME(device, wmHelper) + helper.waitImeWindowGone() + } + } + assertions { + windowManagerTrace { + end("imeWindowAboveApp") { + isAboveWindow(IME_WINDOW_NAME, testApp.defaultWindowName) + } + } + } + } + } + + companion object { + private const val TEST_REPETITIONS = 5 + + @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/PipLegacySplitScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt new file mode 100644 index 000000000000..e5790962c025 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt @@ -0,0 +1,127 @@ +/* + * 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.runFlicker +import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.server.wm.flicker.helpers.exitSplitScreen +import com.android.server.wm.flicker.helpers.isInSplitScreen +import com.android.server.wm.flicker.helpers.launchSplitScreen +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.wm.shell.flicker.helpers.ImeAppHelper +import com.android.wm.shell.flicker.helpers.FixedAppHelper +import com.android.wm.shell.flicker.helpers.PipAppHelper +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP +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 with split-screen. + * To run this test: `atest WMShellFlickerTests:PipLegacySplitScreenTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@FlakyTest(bugId = 161435597) +class PipLegacySplitScreenTest( + rotationName: String, + rotation: Int +) : AppTestBase(rotationName, rotation) { + private val pipApp = PipAppHelper(instrumentation) + private val imeApp = ImeAppHelper(instrumentation) + private val testApp = FixedAppHelper(instrumentation) + + @Test + fun testShowsPipLaunchingToSplitScreen() { + runFlicker(instrumentation) { + withTestName { "testShowsPipLaunchingToSplitScreen" } + repeat { TEST_REPETITIONS } + setup { + test { + removeAllTasksButHome() + device.wakeUpAndGoToHomeScreen() + pipApp.launchViaIntent(stringExtras = mapOf(EXTRA_ENTER_PIP to "true")) + waitForAnimationComplete() + } + } + transitions { + testApp.launchViaIntent() + device.launchSplitScreen() + imeApp.launchViaIntent() + waitForAnimationComplete() + } + teardown { + eachRun { + imeApp.exit() + if (device.isInSplitScreen()) { + device.exitSplitScreen() + } + testApp.exit() + } + test { + removeAllTasksButHome() + } + } + assertions { + val displayBounds = WindowUtils.getDisplayBounds(rotation) + windowManagerTrace { + all("PIP window must remain inside visible bounds") { + coversAtMostRegion(pipApp.defaultWindowName, displayBounds) + } + end("Both app windows should be visible") { + isVisible(testApp.defaultWindowName) + isVisible(imeApp.defaultWindowName) + noWindowsOverlap(setOf(testApp.defaultWindowName, imeApp.defaultWindowName)) + } + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + } + layersTrace { + all("PIP layer must remain inside visible bounds") { + coversAtMostRegion(displayBounds, pipApp.defaultWindowName) + } + end("Both app layers should be visible") { + coversAtMostRegion(displayBounds, testApp.defaultWindowName) + coversAtMostRegion(displayBounds, imeApp.defaultWindowName) + } + navBarLayerIsAlwaysVisible() + statusBarLayerIsAlwaysVisible() + } + } + } + } + + companion object { + const val TEST_REPETITIONS = 2 + @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/PipOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipOrientationTest.kt new file mode 100644 index 000000000000..5e0760ceeda7 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipOrientationTest.kt @@ -0,0 +1,205 @@ +/* + * 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.Intent +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.dsl.runFlicker +import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.wm.shell.flicker.helpers.FixedAppHelper +import com.android.wm.shell.flicker.helpers.PipAppHelper +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_ENTER_PIP +import com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_SET_REQUESTED_ORIENTATION +import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP +import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_PIP_ORIENTATION +import com.android.wm.shell.flicker.testapp.Components.FixedActivity.EXTRA_FIXED_ORIENTATION +import org.junit.Assert.assertEquals +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 with orientation changes. + * To run this test: `atest WMShellFlickerTests:PipOrientationTest` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class PipOrientationTest( + rotationName: String, + rotation: Int +) : AppTestBase(rotationName, rotation) { + // Helper class to process test actions by broadcast. + private inner class BroadcastActionTrigger { + private fun createIntentWithAction(broadcastAction: String): Intent { + return Intent(broadcastAction).setFlags(Intent.FLAG_RECEIVER_FOREGROUND) + } + fun doAction(broadcastAction: String) { + instrumentation.getContext().sendBroadcast(createIntentWithAction(broadcastAction)) + } + fun requestOrientationForPip(orientation: Int) { + instrumentation.getContext() + .sendBroadcast(createIntentWithAction(ACTION_SET_REQUESTED_ORIENTATION) + .putExtra(EXTRA_PIP_ORIENTATION, orientation.toString())) + } + } + private val broadcastActionTrigger = BroadcastActionTrigger() + + // Corresponds to ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + private val ORIENTATION_LANDSCAPE = 0 + // Corresponds to ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + private val ORIENTATION_PORTRAIT = 1 + + private val testApp = FixedAppHelper(instrumentation) + private val pipApp = PipAppHelper(instrumentation) + + @Test + fun testEnterPipToOtherOrientation() { + runFlicker(instrumentation) { + withTestName { "testEnterPipToOtherOrientation" } + setup { + test { + removeAllTasksButHome() + device.wakeUpAndGoToHomeScreen() + // Launch a portrait only app on the fullscreen stack + testApp.launchViaIntent(stringExtras = mapOf( + EXTRA_FIXED_ORIENTATION to ORIENTATION_PORTRAIT.toString())) + waitForAnimationComplete() + // Launch the PiP activity fixed as landscape + pipApp.launchViaIntent(stringExtras = mapOf( + EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString())) + waitForAnimationComplete() + } + } + transitions { + // Enter PiP, and assert that the PiP is within bounds now that the device is back + // in portrait + broadcastActionTrigger.doAction(ACTION_ENTER_PIP) + waitForAnimationComplete() + } + teardown { + test { + removeAllTasksButHome() + } + } + assertions { + windowManagerTrace { + all("pipApp window is always on top") { + showsAppWindowOnTop(pipApp.defaultWindowName) + } + start("pipApp window hides testApp") { + isInvisible(testApp.defaultWindowName) + } + end("testApp windows is shown") { + isVisible(testApp.defaultWindowName) + } + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + } + layersTrace { + val startingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_90) + val endingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_0) + start("pipApp layer hides testApp") { + hasVisibleRegion(pipApp.defaultWindowName, startingBounds) + isInvisible(testApp.defaultWindowName) + } + end("testApp layer covers fullscreen", enabled = false) { + hasVisibleRegion(testApp.defaultWindowName, endingBounds) + } + navBarLayerIsAlwaysVisible(bugId = 140855415) + statusBarLayerIsAlwaysVisible(bugId = 140855415) + } + } + } + } + + @Test + fun testSetRequestedOrientationWhilePinned() { + runFlicker(instrumentation) { + withTestName { "testSetRequestedOrientationWhilePinned" } + setup { + test { + removeAllTasksButHome() + device.wakeUpAndGoToHomeScreen() + // Launch the PiP activity fixed as landscape + pipApp.launchViaIntent(stringExtras = mapOf( + EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString(), + EXTRA_ENTER_PIP to "true")) + waitForAnimationComplete() + assertEquals(Surface.ROTATION_0, device.displayRotation) + } + } + transitions { + // Request that the orientation is set to landscape + broadcastActionTrigger.requestOrientationForPip(ORIENTATION_LANDSCAPE) + + // Launch the activity back into fullscreen and ensure that it is now in landscape + pipApp.launchViaIntent() + waitForAnimationComplete() + assertEquals(Surface.ROTATION_90, device.displayRotation) + } + teardown { + test { + removeAllTasksButHome() + } + } + assertions { + val startingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_0) + val endingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_90) + windowManagerTrace { + start("PIP window must remain inside display") { + coversAtMostRegion(pipApp.defaultWindowName, startingBounds) + } + end("pipApp shows on top") { + showsAppWindowOnTop(pipApp.defaultWindowName) + } + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + } + layersTrace { + start("PIP layer must remain inside display") { + coversAtMostRegion(startingBounds, pipApp.defaultWindowName) + } + end("pipApp layer covers fullscreen") { + hasVisibleRegion(pipApp.defaultWindowName, endingBounds) + } + navBarLayerIsAlwaysVisible(bugId = 140855415) + statusBarLayerIsAlwaysVisible(bugId = 140855415) + } + } + } + } + + 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/PipRotationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt new file mode 100644 index 000000000000..a00c5f463a50 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt @@ -0,0 +1,127 @@ +/* + * 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.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.repetitions +import com.android.server.wm.flicker.startRotation +import com.android.wm.shell.flicker.helpers.FixedAppHelper +import com.android.wm.shell.flicker.helpers.PipAppHelper +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.noUncoveredRegions +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test Pip Stack in bounds after rotations. + * To run this test: `atest WMShellFlickerTests:PipRotationTest` + */ +@Presubmit +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class PipRotationTest( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any>> { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val testApp = FixedAppHelper(instrumentation) + val pipApp = PipAppHelper(instrumentation) + return FlickerTestRunnerFactory.getInstance().buildRotationTest(instrumentation, + supportedRotations = listOf(Surface.ROTATION_0, Surface.ROTATION_90)) { + configuration -> + withTestName { buildTestTag("PipRotationTest", testApp, configuration) } + repeat { configuration.repetitions } + setup { + test { + AppTestBase.removeAllTasksButHome() + device.wakeUpAndGoToHomeScreen() + pipApp.launchViaIntent(stringExtras = mapOf( + EXTRA_ENTER_PIP to "true")) + testApp.launchViaIntent() + AppTestBase.waitForAnimationComplete() + } + eachRun { + setRotation(configuration.startRotation) + } + } + transitions { + setRotation(configuration.endRotation) + } + teardown { + eachRun { + setRotation(Surface.ROTATION_0) + } + test { + AppTestBase.removeAllTasksButHome() + } + } + assertions { + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + } + layersTrace { + navBarLayerIsAlwaysVisible(bugId = 140855415) + statusBarLayerIsAlwaysVisible(bugId = 140855415) + noUncoveredRegions(configuration.startRotation, + configuration.endRotation, allStates = false) + navBarLayerRotatesAndScales(configuration.startRotation, + configuration.endRotation, bugId = 140855415) + statusBarLayerRotatesScales(configuration.startRotation, + configuration.endRotation, bugId = 140855415) + } + layersTrace { + val startingBounds = WindowUtils.getDisplayBounds( + configuration.startRotation) + val endingBounds = WindowUtils.getDisplayBounds( + configuration.endRotation) + start("appLayerRotates_StartingBounds", bugId = 140855415) { + hasVisibleRegion(testApp.defaultWindowName, startingBounds) + coversAtMostRegion(startingBounds, pipApp.defaultWindowName) + } + end("appLayerRotates_EndingBounds", bugId = 140855415) { + hasVisibleRegion(testApp.defaultWindowName, endingBounds) + coversAtMostRegion(endingBounds, pipApp.defaultWindowName) + } + } + } + } + } + } +}
\ 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..96b6c912d152 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTestBase.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.pip + +import com.android.wm.shell.flicker.helpers.PipAppHelper +import org.junit.Before + +abstract class PipTestBase( + rotationName: String, + rotation: Int +) : AppTestBase(rotationName, rotation) { + protected val testApp = PipAppHelper(instrumentation) + + @Before + override fun televisionSetUp() { + /** + * The super implementation assumes ([org.junit.Assume]) that not running on TV, thus + * disabling the test on TV. This test, however, *should run on TV*, so we overriding this + * method and simply leaving it blank. + */ + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt new file mode 100644 index 000000000000..3e7eb134e627 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt @@ -0,0 +1,133 @@ +/* + * 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 androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.focusChanges +import com.android.server.wm.flicker.helpers.buildTestTag +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.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.noUncoveredRegions +import com.android.server.wm.flicker.repetitions +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.helpers.PipAppHelper +import org.junit.FixMethodOrder +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 PipToAppTest( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<Array<Any>> { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val testApp = PipAppHelper(instrumentation) + return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, + supportedRotations = listOf(Surface.ROTATION_0)) { configuration -> + withTestName { buildTestTag("exitPipModeToApp", testApp, configuration) } + repeat { configuration.repetitions } + setup { + test { + device.wakeUpAndGoToHomeScreen() + device.pressHome() + testApp.launchViaIntent(wmHelper) + } + eachRun { + this.setRotation(configuration.startRotation) + testApp.clickEnterPipButton() + device.hasPipWindow() + } + } + teardown { + eachRun { + this.setRotation(Surface.ROTATION_0) + } + test { + if (device.hasPipWindow()) { + device.closePipWindow() + } + testApp.exit() + } + } + transitions { + device.expandPipWindow() + device.waitForIdle() + } + assertions { + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + + all("appReplacesPipWindow") { + this.showsAppWindow(PIP_WINDOW_TITLE) + .then() + .showsAppWindowOnTop(testApp.launcherName) + } + } + + layersTrace { + navBarLayerIsAlwaysVisible(bugId = 140855415) + statusBarLayerIsAlwaysVisible() + noUncoveredRegions(configuration.startRotation, Surface.ROTATION_0, + enabled = false) + navBarLayerRotatesAndScales(configuration.startRotation, + Surface.ROTATION_0, bugId = 140855415) + statusBarLayerRotatesScales(configuration.startRotation, + Surface.ROTATION_0) + + all("appReplacesPipLayer") { + this.showsLayer(PIP_WINDOW_TITLE) + .then() + .showsLayer(testApp.launcherName) + } + } + + eventLog { + focusChanges( + "NexusLauncherActivity", testApp.launcherName, + "NexusLauncherActivity", bugId = 151179149) + } + } + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToHomeTest.kt new file mode 100644 index 000000000000..5d3bc1388686 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToHomeTest.kt @@ -0,0 +1,133 @@ +/* + * 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 androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerTestRunner +import com.android.server.wm.flicker.FlickerTestRunnerFactory +import com.android.server.wm.flicker.focusChanges +import com.android.server.wm.flicker.helpers.buildTestTag +import com.android.server.wm.flicker.helpers.closePipWindow +import com.android.server.wm.flicker.helpers.hasPipWindow +import com.android.server.wm.flicker.helpers.setRotation +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.noUncoveredRegions +import com.android.server.wm.flicker.repetitions +import com.android.server.wm.flicker.startRotation +import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.wm.shell.flicker.helpers.PipAppHelper +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test Pip launch. + * To run this test: `atest WMShellFlickerTests:PipToHomeTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@FlakyTest(bugId = 152738416) +class PipToHomeTest( + testSpec: FlickerTestRunnerFactory.TestSpec +) : FlickerTestRunner(testSpec) { + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<Array<Any>> { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val testApp = PipAppHelper(instrumentation) + return FlickerTestRunnerFactory.getInstance().buildTest(instrumentation, + supportedRotations = listOf(Surface.ROTATION_0)) { configuration -> + withTestName { buildTestTag("exitPipModeToApp", testApp, configuration) } + repeat { configuration.repetitions } + setup { + test { + device.wakeUpAndGoToHomeScreen() + device.pressHome() + } + eachRun { + testApp.launchViaIntent(wmHelper) + this.setRotation(configuration.startRotation) + testApp.clickEnterPipButton() + device.hasPipWindow() + } + } + teardown { + eachRun { + this.setRotation(Surface.ROTATION_0) + if (device.hasPipWindow()) { + device.closePipWindow() + } + } + test { + if (device.hasPipWindow()) { + device.closePipWindow() + } + testApp.exit() + } + } + transitions { + testApp.closePipWindow() + } + assertions { + windowManagerTrace { + navBarWindowIsAlwaysVisible() + statusBarWindowIsAlwaysVisible() + + all("pipWindowBecomesInvisible") { + this.showsAppWindow(PIP_WINDOW_TITLE) + .then() + .hidesAppWindow(PIP_WINDOW_TITLE) + } + } + + layersTrace { + navBarLayerIsAlwaysVisible(bugId = 140855415) + statusBarLayerIsAlwaysVisible() + noUncoveredRegions(configuration.startRotation, Surface.ROTATION_0, + enabled = false) + navBarLayerRotatesAndScales(configuration.startRotation, + Surface.ROTATION_0, bugId = 140855415) + statusBarLayerRotatesScales(configuration.startRotation, + Surface.ROTATION_0) + + all("pipLayerBecomesInvisible") { + this.showsLayer(PIP_WINDOW_TITLE) + .then() + .hidesLayer(PIP_WINDOW_TITLE) + } + } + + eventLog { + focusChanges(testApp.launcherName, "NexusLauncherActivity", + bugId = 151179149) + } + } + } + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt new file mode 100644 index 000000000000..49094e609fbc --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt @@ -0,0 +1,87 @@ +/* + * 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.tv + +import android.graphics.Rect +import android.util.Rational +import androidx.test.filters.RequiresDevice +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** + * Test Pip Menu on TV. + * To run this test: `atest WMShellFlickerTests:TvPipBasicTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +class TvPipBasicTest( + private val radioButtonId: String, + private val pipWindowRatio: Rational? +) : TvPipTestBase() { + + @Test + fun enterPip_openMenu_pressBack_closePip() { + // Launch test app + testApp.launchViaIntent() + + // Set up ratio and enter Pip + testApp.clickObject(radioButtonId) + testApp.clickEnterPipButton() + + val actualRatio: Float = testApp.ui?.visibleBounds?.ratio + ?: fail("Application UI not found") + pipWindowRatio?.let { expectedRatio -> + assertEquals("Wrong Pip window ratio", expectedRatio.toFloat(), actualRatio) + } + + // Pressing the Window key should bring up Pip menu + uiDevice.pressWindowKey() + uiDevice.waitForTvPipMenu() ?: fail("Pip menu should have been shown") + + // Pressing the Back key should close the Pip menu + uiDevice.pressBack() + assertTrue("Pip menu should have closed", uiDevice.waitForTvPipMenuToClose()) + + // Make sure Pip Window ration remained the same after Pip menu was closed + testApp.ui?.visibleBounds?.let { newBounds -> + assertEquals("Pip window ratio has changed", actualRatio, newBounds.ratio) + } ?: fail("Application UI not found") + + // Close Pip + testApp.closePipWindow() + } + + private val Rect.ratio: Float + get() = width().toFloat() / height() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<Array<Any?>> { + infix fun Int.to(denominator: Int) = Rational(this, denominator) + return listOf( + arrayOf("ratio_default", null), + arrayOf("ratio_square", 1 to 1), + arrayOf("ratio_wide", 2 to 1), + arrayOf("ratio_tall", 1 to 2) + ) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt new file mode 100644 index 000000000000..0110ba3f5b30 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt @@ -0,0 +1,256 @@ +/* + * 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.tv + +import android.graphics.Rect +import androidx.test.filters.RequiresDevice +import androidx.test.uiautomator.UiObject2 +import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME +import com.android.wm.shell.flicker.testapp.Components +import com.android.wm.shell.flicker.wait +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +/** + * Test Pip Menu on TV. + * To run this test: `atest WMShellFlickerTests:TvPipMenuTests` + */ +@RequiresDevice +class TvPipMenuTests : TvPipTestBase() { + + private val systemUiResources = + packageManager.getResourcesForApplication(SYSTEM_UI_PACKAGE_NAME) + private val pipBoundsWhileInMenu: Rect = systemUiResources.run { + val bounds = getString(getIdentifier("pip_menu_bounds", "string", SYSTEM_UI_PACKAGE_NAME)) + Rect.unflattenFromString(bounds) ?: error("Could not retrieve PiP menu bounds") + } + private val playButtonDescription = systemUiResources.run { + getString(getIdentifier("pip_play", "string", SYSTEM_UI_PACKAGE_NAME)) + } + private val pauseButtonDescription = systemUiResources.run { + getString(getIdentifier("pip_pause", "string", SYSTEM_UI_PACKAGE_NAME)) + } + + @Before + fun tvPipMenuTestsTestUp() { + // Launch the app and go to PiP + testApp.launchViaIntent() + } + + @Test + fun pipMenu_correctPosition() { + enterPip_openMenu_assertShown() + + // Make sure the PiP task is positioned where it should be. + val activityBounds: Rect = testApp.ui?.visibleBounds + ?: error("Could not retrieve Pip Activity bounds") + assertTrue("Pip Activity is positioned correctly while Pip menu is shown", + pipBoundsWhileInMenu == activityBounds) + + // Make sure the Pip Menu Actions are positioned correctly. + uiDevice.findTvPipMenuControls()?.visibleBounds?.run { + assertTrue("Pip Menu Actions should be positioned below the Activity in Pip", + top >= activityBounds.bottom) + assertTrue("Pip Menu Actions should be positioned central horizontally", + centerX() == uiDevice.displayWidth / 2) + assertTrue("Pip Menu Actions should be fully shown on the screen", + left >= 0 && right <= uiDevice.displayWidth && bottom <= uiDevice.displayHeight) + } ?: error("Could not retrieve Pip Menu Actions bounds") + + testApp.closePipWindow() + } + + @Test + fun pipMenu_backButton() { + enterPip_openMenu_assertShown() + + // Pressing the Back key should close the Pip menu + uiDevice.pressBack() + assertTrue("Pip menu should have closed", uiDevice.waitForTvPipMenuToClose()) + + testApp.closePipWindow() + } + + @Test + fun pipMenu_homeButton() { + enterPip_openMenu_assertShown() + + // Pressing the Home key should close the Pip menu + uiDevice.pressHome() + assertTrue("Pip menu should have closed", uiDevice.waitForTvPipMenuToClose()) + + testApp.closePipWindow() + } + + @Test + fun pipMenu_closeButton() { + enterPip_openMenu_assertShown() + + // PiP menu should contain the Close button + uiDevice.findTvPipMenuCloseButton() + ?: fail("\"Close PIP\" button should be shown in Pip menu") + + // Clicking on the Close button should close the app + uiDevice.clickTvPipMenuCloseButton() + assertTrue("\"Close PIP\" button should close the PiP", testApp.waitUntilClosed()) + } + + @Test + fun pipMenu_fullscreenButton() { + enterPip_openMenu_assertShown() + + // PiP menu should contain the Fullscreen button + uiDevice.findTvPipMenuFullscreenButton() + ?: fail("\"Full screen\" button should be shown in Pip menu") + + // Clicking on the fullscreen button should return app to the fullscreen mode. + // Click, wait for the app to go fullscreen + uiDevice.clickTvPipMenuFullscreenButton() + assertTrue("\"Full screen\" button should open the app fullscreen", + wait { testApp.ui?.isFullscreen(uiDevice) ?: false }) + + // Close the app + uiDevice.pressBack() + testApp.waitUntilClosed() + } + + @Test + fun pipMenu_mediaPlayPauseButtons() { + // Start media session before entering PiP + testApp.clickStartMediaSessionButton() + + enterPip_openMenu_assertShown() + assertFullscreenAndCloseButtonsAreShown() + + // PiP menu should contain the Pause button + uiDevice.findTvPipMenuElementWithDescription(pauseButtonDescription) + ?: fail("\"Pause\" button should be shown in Pip menu if there is an active " + + "playing media session.") + + // When we pause media, the button should change from Pause to Play + uiDevice.clickTvPipMenuElementWithDescription(pauseButtonDescription) + + assertFullscreenAndCloseButtonsAreShown() + // PiP menu should contain the Play button now + uiDevice.waitForTvPipMenuElementWithDescription(playButtonDescription) + ?: fail("\"Play\" button should be shown in Pip menu if there is an active " + + "paused media session.") + + testApp.closePipWindow() + } + + @Test + fun pipMenu_withCustomActions() { + // Enter PiP with custom actions. + testApp.checkWithCustomActionsCheckbox() + enterPip_openMenu_assertShown() + + // PiP menu should contain "No-Op", "Off" and "Clear" buttons... + uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_NO_OP) + ?: fail("\"No-Op\" button should be shown in Pip menu") + uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_OFF) + ?: fail("\"Off\" button should be shown in Pip menu") + uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_CLEAR) + ?: fail("\"Clear\" button should be shown in Pip menu") + // ... and should also contain the "Full screen" and "Close" buttons. + assertFullscreenAndCloseButtonsAreShown() + + uiDevice.clickTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_OFF) + // Invoking the "Off" action should replace it with the "On" action/button and should + // remove the "No-Op" action/button. "Clear" action/button should remain in the menu ... + uiDevice.waitForTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_ON) + ?: fail("\"On\" button should be shown in Pip for a corresponding custom action") + assertNull("\"No-Op\" button should not be shown in Pip menu", + uiDevice.findTvPipMenuElementWithDescription( + Components.PipActivity.MENU_ACTION_NO_OP)) + uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_CLEAR) + ?: fail("\"Clear\" button should be shown in Pip menu") + // ... as well as the "Full screen" and "Close" buttons. + assertFullscreenAndCloseButtonsAreShown() + + uiDevice.clickTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_CLEAR) + // Invoking the "Clear" action should remove all the custom actions and their corresponding + // buttons, ... + uiDevice.waitUntilTvPipMenuElementWithDescriptionIsGone( + Components.PipActivity.MENU_ACTION_ON)?.also { + isGone -> if (!isGone) fail("\"On\" button should not be shown in Pip menu") + } + assertNull("\"Off\" button should not be shown in Pip menu", + uiDevice.findTvPipMenuElementWithDescription( + Components.PipActivity.MENU_ACTION_OFF)) + assertNull("\"Clear\" button should not be shown in Pip menu", + uiDevice.findTvPipMenuElementWithDescription( + Components.PipActivity.MENU_ACTION_CLEAR)) + assertNull("\"No-Op\" button should not be shown in Pip menu", + uiDevice.findTvPipMenuElementWithDescription( + Components.PipActivity.MENU_ACTION_NO_OP)) + // ... but the menu should still contain the "Full screen" and "Close" buttons. + assertFullscreenAndCloseButtonsAreShown() + + testApp.closePipWindow() + } + + @Test + fun pipMenu_customActions_override_mediaControls() { + // Start media session before entering PiP with custom actions. + testApp.checkWithCustomActionsCheckbox() + testApp.clickStartMediaSessionButton() + enterPip_openMenu_assertShown() + + // PiP menu should contain "No-Op", "Off" and "Clear" buttons for the custom actions... + uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_NO_OP) + ?: fail("\"No-Op\" button should be shown in Pip menu") + uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_OFF) + ?: fail("\"Off\" button should be shown in Pip menu") + uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_CLEAR) + ?: fail("\"Clear\" button should be shown in Pip menu") + // ... should also contain the "Full screen" and "Close" buttons, ... + assertFullscreenAndCloseButtonsAreShown() + // ... but should not contain media buttons. + assertNull("\"Play\" button should not be shown in menu when there are custom actions", + uiDevice.findTvPipMenuElementWithDescription(playButtonDescription)) + assertNull("\"Pause\" button should not be shown in menu when there are custom actions", + uiDevice.findTvPipMenuElementWithDescription(pauseButtonDescription)) + + uiDevice.clickTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_CLEAR) + // Invoking the "Clear" action should remove all the custom actions, which should bring up + // media buttons... + uiDevice.waitForTvPipMenuElementWithDescription(pauseButtonDescription) + ?: fail("\"Pause\" button should be shown in Pip menu if there is an active " + + "playing media session.") + // ... while the "Full screen" and "Close" buttons should remain in the menu. + assertFullscreenAndCloseButtonsAreShown() + + testApp.closePipWindow() + } + + private fun enterPip_openMenu_assertShown(): UiObject2 { + testApp.clickEnterPipButton() + // Pressing the Window key should bring up Pip menu + uiDevice.pressWindowKey() + return uiDevice.waitForTvPipMenu() ?: fail("Pip menu should have been shown") + } + + private fun assertFullscreenAndCloseButtonsAreShown() { + uiDevice.findTvPipMenuCloseButton() + ?: fail("\"Close PIP\" button should be shown in Pip menu") + uiDevice.findTvPipMenuFullscreenButton() + ?: fail("\"Full screen\" button should be shown in Pip menu") + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt new file mode 100644 index 000000000000..bcf38d340867 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt @@ -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.flicker.pip.tv + +import android.app.Notification +import android.app.PendingIntent +import android.os.Bundle +import android.service.notification.StatusBarNotification +import androidx.test.filters.RequiresDevice +import com.android.wm.shell.flicker.NotificationListener.Companion.findNotification +import com.android.wm.shell.flicker.NotificationListener.Companion.startNotificationListener +import com.android.wm.shell.flicker.NotificationListener.Companion.stopNotificationListener +import com.android.wm.shell.flicker.NotificationListener.Companion.waitForNotificationToAppear +import com.android.wm.shell.flicker.NotificationListener.Companion.waitForNotificationToDisappear +import org.junit.After +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +/** + * Test Pip Notifications on TV. + * To run this test: `atest WMShellFlickerTests:TvPipNotificationTests` + */ +@RequiresDevice +class TvPipNotificationTests : TvPipTestBase() { + @Before + fun tvPipNotificationTestsSetUp() { + val started = startNotificationListener() + if (!started) { + error("NotificationListener hasn't started") + } + } + + @After + override fun tearDown() { + stopNotificationListener() + super.tearDown() + } + + @Test + fun pipNotification_postedAndDismissed() { + testApp.launchViaIntent() + testApp.clickEnterPipButton() + + assertNotNull("Pip notification should have been posted", + waitForNotificationToAppear { it.isPipNotificationWithTitle(testApp.appName) }) + + testApp.closePipWindow() + + assertTrue("Pip notification should have been dismissed", + waitForNotificationToDisappear { it.isPipNotificationWithTitle(testApp.appName) }) + } + + @Test + fun pipNotification_closeIntent() { + testApp.launchViaIntent() + testApp.clickEnterPipButton() + + val notification: StatusBarNotification = waitForNotificationToAppear { + it.isPipNotificationWithTitle(testApp.appName) + } ?: fail("Pip notification should have been posted") + + notification.deleteIntent?.send() + ?: fail("Pip notification should contain `delete_intent`") + + assertTrue("Pip should have closed by sending the `delete_intent`", + testApp.waitUntilClosed()) + assertTrue("Pip notification should have been dismissed", + waitForNotificationToDisappear { it.isPipNotificationWithTitle(testApp.appName) }) + } + + @Test + fun pipNotification_menuIntent() { + testApp.launchViaIntent() + testApp.clickEnterPipButton() + + val notification: StatusBarNotification = waitForNotificationToAppear { + it.isPipNotificationWithTitle(testApp.appName) + } ?: fail("Pip notification should have been posted") + + notification.contentIntent?.send() + ?: fail("Pip notification should contain `content_intent`") + + assertNotNull("Pip menu should have been shown after sending `content_intent`", + uiDevice.waitForTvPipMenu()) + + uiDevice.pressBack() + testApp.closePipWindow() + } + + @Test + fun pipNotification_mediaSessionTitle_isDisplayed() { + testApp.launchViaIntent() + // Start media session and to PiP + testApp.clickStartMediaSessionButton() + testApp.clickEnterPipButton() + + // Wait for the correct notification to show up... + waitForNotificationToAppear { + it.isPipNotificationWithTitle(TITLE_MEDIA_SESSION_PLAYING) + } ?: fail("Pip notification with media session title should have been posted") + // ... and make sure "regular" PiP notification is now shown + assertNull("Regular notification should not have been posted", + findNotification { it.isPipNotificationWithTitle(testApp.appName) }) + + // Pause the media session. When paused the application updates the title for the media + // session. This change should be reflected in the notification. + testApp.pauseMedia() + + // Wait for the "paused" notification to show up... + waitForNotificationToAppear { + it.isPipNotificationWithTitle(TITLE_MEDIA_SESSION_PAUSED) + } ?: fail("Pip notification with media session title should have been posted") + // ... and make sure "playing" PiP notification is gone + assertNull("Regular notification should not have been posted", + findNotification { it.isPipNotificationWithTitle(TITLE_MEDIA_SESSION_PLAYING) }) + + // Now stop the media session, which should revert the title to the "default" one. + testApp.stopMedia() + + // Wait for the "regular" notification to show up... + waitForNotificationToAppear { + it.isPipNotificationWithTitle(testApp.appName) + } ?: fail("Pip notification with media session title should have been posted") + // ... and make sure previous ("paused") notification is gone + assertNull("Regular notification should not have been posted", + findNotification { it.isPipNotificationWithTitle(TITLE_MEDIA_SESSION_PAUSED) }) + + testApp.closePipWindow() + } + + companion object { + private const val TITLE_MEDIA_SESSION_PLAYING = "TestApp media is playing" + private const val TITLE_MEDIA_SESSION_PAUSED = "TestApp media is paused" + } +} + +private val StatusBarNotification.extras: Bundle? + get() = notification?.extras + +private val StatusBarNotification.title: String + get() = extras?.getString(Notification.EXTRA_TITLE) ?: "" + +/** Get TV extensions with [android.app.Notification.TvExtender.EXTRA_TV_EXTENDER]. */ +private val StatusBarNotification.tvExtensions: Bundle? + get() = extras?.getBundle("android.tv.EXTENSIONS") + +/** "Content" TV intent with key [android.app.Notification.TvExtender.EXTRA_CONTENT_INTENT]. */ +private val StatusBarNotification.contentIntent: PendingIntent? + get() = tvExtensions?.getParcelable("content_intent") + +/** "Delete" TV intent with key [android.app.Notification.TvExtender.EXTRA_DELETE_INTENT]. */ +private val StatusBarNotification.deleteIntent: PendingIntent? + get() = tvExtensions?.getParcelable("delete_intent") + +private fun StatusBarNotification.isPipNotificationWithTitle(expectedTitle: String): Boolean = + tag == "TvPip" && title == expectedTitle
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt new file mode 100644 index 000000000000..31e9167c79b2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip.tv + +import android.app.ActivityManager +import android.app.IActivityManager +import android.app.IProcessObserver +import android.os.SystemClock +import android.view.Surface.ROTATION_0 +import android.view.Surface.rotationToString +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME +import com.android.wm.shell.flicker.pip.PipTestBase +import org.junit.After +import org.junit.Assert.assertFalse +import org.junit.Assume +import org.junit.Before + +abstract class TvPipTestBase : PipTestBase(rotationToString(ROTATION_0), ROTATION_0) { + + private val systemUiProcessObserver = SystemUiProcessObserver() + + @Before + final override fun televisionSetUp() { + // Should run only on TVs. + Assume.assumeTrue(isTelevision) + + systemUiProcessObserver.start() + + uiDevice.wakeUpAndGoToHomeScreen() + } + + @After + open fun tearDown() { + if (!isTelevision) return + + testApp.exit() + + // Wait for 1 second, and check if the SystemUI has been alive and well since the start. + SystemClock.sleep(AFTER_TEXT_PROCESS_CHECK_DELAY) + systemUiProcessObserver.stop() + assertFalse("SystemUI has died during test execution", systemUiProcessObserver.hasDied) + } + + protected fun fail(message: String): Nothing = throw AssertionError(message) + + inner class SystemUiProcessObserver : IProcessObserver.Stub() { + private val activityManager: IActivityManager = ActivityManager.getService() + private val uiAutomation = instrumentation.uiAutomation + private val systemUiUid = packageManager.getPackageUid(SYSTEM_UI_PACKAGE_NAME, 0) + var hasDied: Boolean = false + + fun start() { + hasDied = false + uiAutomation.adoptShellPermissionIdentity( + android.Manifest.permission.SET_ACTIVITY_WATCHER) + activityManager.registerProcessObserver(this) + } + + fun stop() { + activityManager.unregisterProcessObserver(this) + uiAutomation.dropShellPermissionIdentity() + } + + override fun onForegroundActivitiesChanged(pid: Int, uid: Int, foreground: Boolean) {} + + override fun onForegroundServicesChanged(pid: Int, uid: Int, serviceTypes: Int) {} + + override fun onProcessDied(pid: Int, uid: Int) { + if (uid == systemUiUid) hasDied = true + } + } + + companion object { + private const val AFTER_TEXT_PROCESS_CHECK_DELAY = 1_000L // 1 sec + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt new file mode 100644 index 000000000000..1b73920046dc --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt @@ -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.flicker.pip.tv + +import android.view.KeyEvent +import androidx.test.uiautomator.By +import androidx.test.uiautomator.BySelector +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiObject2 +import androidx.test.uiautomator.Until +import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME + +/** Id of the root view in the com.android.wm.shell.pip.tv.PipMenuActivity */ +private const val TV_PIP_MENU_ROOT_ID = "tv_pip_menu" +private const val TV_PIP_MENU_BUTTONS_CONTAINER_ID = "tv_pip_menu_action_buttons" +private const val TV_PIP_MENU_CLOSE_BUTTON_ID = "tv_pip_menu_close_button" +private const val TV_PIP_MENU_FULLSCREEN_BUTTON_ID = "tv_pip_menu_fullscreen_button" + +private const val FOCUS_ATTEMPTS = 10 +private const val WAIT_TIME_MS = 3_000L + +private val TV_PIP_MENU_SELECTOR = + By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_ROOT_ID) +private val TV_PIP_MENU_BUTTONS_CONTAINER_SELECTOR = + By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_BUTTONS_CONTAINER_ID) +private val TV_PIP_MENU_CLOSE_BUTTON_SELECTOR = + By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_CLOSE_BUTTON_ID) +private val TV_PIP_MENU_FULLSCREEN_BUTTON_SELECTOR = + By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_FULLSCREEN_BUTTON_ID) + +fun UiDevice.waitForTvPipMenu(): UiObject2? = + wait(Until.findObject(TV_PIP_MENU_SELECTOR), WAIT_TIME_MS) + +fun UiDevice.waitForTvPipMenuToClose(): Boolean = + wait(Until.gone(TV_PIP_MENU_SELECTOR), WAIT_TIME_MS) + +fun UiDevice.findTvPipMenuControls(): UiObject2? = + findTvPipMenuElement(TV_PIP_MENU_BUTTONS_CONTAINER_SELECTOR) + +fun UiDevice.findTvPipMenuCloseButton(): UiObject2? = + findTvPipMenuElement(TV_PIP_MENU_CLOSE_BUTTON_SELECTOR) + +fun UiDevice.findTvPipMenuFullscreenButton(): UiObject2? = + findTvPipMenuElement(TV_PIP_MENU_FULLSCREEN_BUTTON_SELECTOR) + +fun UiDevice.findTvPipMenuElementWithDescription(desc: String): UiObject2? = + findTvPipMenuElement(By.desc(desc)) + +private fun UiDevice.findTvPipMenuElement(selector: BySelector): UiObject2? = + findObject(TV_PIP_MENU_SELECTOR)?.findObject(selector) + +fun UiDevice.waitForTvPipMenuElementWithDescription(desc: String): UiObject2? { + // Ideally, we'd want to wait for an element with the given description that has the Pip Menu as + // its parent, but the API does not allow us to construct a query exactly that way. + // So instead we'll wait for a Pip Menu that has the element, which we are looking for, as a + // descendant and then retrieve the element from the menu and return to the caller of this + // method. + val elementSelector = By.desc(desc) + val menuContainingElementSelector = By.copy(TV_PIP_MENU_SELECTOR).hasDescendant(elementSelector) + + return wait(Until.findObject(menuContainingElementSelector), WAIT_TIME_MS) + ?.findObject(elementSelector) +} + +fun UiDevice.waitUntilTvPipMenuElementWithDescriptionIsGone(desc: String): Boolean? { + val elementSelector = By.desc(desc) + val menuContainingElementSelector = By.copy(TV_PIP_MENU_SELECTOR).hasDescendant(elementSelector) + + return wait(Until.gone(menuContainingElementSelector), WAIT_TIME_MS) +} + +fun UiDevice.clickTvPipMenuCloseButton() { + focusOnAndClickTvPipMenuElement(TV_PIP_MENU_CLOSE_BUTTON_SELECTOR) || + error("Could not focus on the Close button") +} + +fun UiDevice.clickTvPipMenuFullscreenButton() { + focusOnAndClickTvPipMenuElement(TV_PIP_MENU_FULLSCREEN_BUTTON_SELECTOR) || + error("Could not focus on the Fullscreen button") +} + +fun UiDevice.clickTvPipMenuElementWithDescription(desc: String) { + focusOnAndClickTvPipMenuElement(By.desc(desc).pkg(SYSTEM_UI_PACKAGE_NAME)) || + error("Could not focus on the Pip menu object with \"$desc\" description") + // So apparently Accessibility framework on TV is not very reliable and sometimes the state of + // the tree of accessibility nodes as seen by the accessibility clients kind of lags behind of + // the "real" state of the "UI tree". It seems, however, that moving focus around the tree + // forces the AccessibilityNodeInfo tree to get properly updated. + // So since we suspect that clicking on a Pip Menu element may cause some UI changes and we want + // those changes to be seen by the UiAutomator, which is using Accessibility framework under the + // hood for inspecting UI, we'll move the focus around a little. + moveFocus() +} + +private fun UiDevice.focusOnAndClickTvPipMenuElement(selector: BySelector): Boolean { + repeat(FOCUS_ATTEMPTS) { + val element = findTvPipMenuElement(selector) + ?: error("The Pip Menu element we try to focus on is gone.") + + if (element.isFocusedOrHasFocusedChild) { + pressDPadCenter() + return true + } + + findTvPipMenuElement(By.focused(true))?.let { focused -> + if (element.visibleCenter.x < focused.visibleCenter.x) + pressDPadLeft() else pressDPadRight() + waitForIdle() + } ?: error("Pip menu does not contain a focused element") + } + + return false +} + +fun UiDevice.closeTvPipWindow() { + // Check if Pip menu is Open. If it's not, open it. + if (findObject(TV_PIP_MENU_SELECTOR) == null) { + pressWindowKey() + waitForTvPipMenu() ?: error("Could not open Pip menu") + } + + clickTvPipMenuCloseButton() + waitForTvPipMenuToClose() +} + +/** + * Simply presses the D-Pad Left and Right buttons once, which should move the focus on the screen, + * which should cause Accessibility events to be fired, which should, hopefully, properly update + * AccessibilityNodeInfo tree dispatched by the platform to the Accessibility services, one of which + * is the UiAutomator. + */ +private fun UiDevice.moveFocus() { + waitForIdle() + pressDPadLeft() + waitForIdle() + pressDPadRight() + waitForIdle() +} + +fun UiDevice.pressWindowKey() = pressKeyCode(KeyEvent.KEYCODE_WINDOW) + +fun UiObject2.isFullscreen(uiDevice: UiDevice): Boolean = visibleBounds.run { + height() == uiDevice.displayHeight && width() == uiDevice.displayWidth +} + +val UiObject2.isFocusedOrHasFocusedChild: Boolean + get() = isFocused || findObject(By.focused(true)) != null 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..26627a47ee62 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp @@ -0,0 +1,26 @@ +// 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"], +} + +java_library { + name: "wmshell-flicker-test-components", + srcs: ["src/**/Components.java"], + sdk_version: "test_current", +}
\ No newline at end of file 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..5549330df766 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml @@ -0,0 +1,111 @@ +<?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=".FixedActivity" + android:resizeableActivity="true" + android:supportsPictureInPicture="true" + android:launchMode="singleTop" + android:label="FixedApp" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + <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:launchMode="singleTop" + 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:launchMode="singleTop" + 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=".SplitScreenActivity" + android:resizeableActivity="true" + android:taskAffinity="com.android.wm.shell.flicker.testapp.SplitScreenActivity" + android:label="SplitScreenPrimaryApp" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + + <activity android:name=".SplitScreenSecondaryActivity" + android:resizeableActivity="true" + android:taskAffinity="com.android.wm.shell.flicker.testapp.SplitScreenSecondaryActivity" + android:label="SplitScreenSecondaryApp" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + + <activity android:name=".NonResizeableActivity" + android:resizeableActivity="false" + android:taskAffinity="com.android.wm.shell.flicker.testapp.NonResizeableActivity" + android:label="NonResizeableApp" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + + <activity android:name=".SimpleActivity" + android:taskAffinity="com.android.wm.shell.flicker.testapp.SimpleActivity" + android:label="SimpleApp" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.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_non_resizeable.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_non_resizeable.xml new file mode 100644 index 000000000000..45d5917f86d6 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_non_resizeable.xml @@ -0,0 +1,32 @@ +<?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:orientation="vertical" + android:background="@android:color/holo_orange_light"> + + <TextView + android:id="@+id/NonResizeableTest" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:gravity="center_vertical|center_horizontal" + android:text="NonResizeableActivity" + android:textAppearance="?android:attr/textAppearanceLarge"/> + +</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..909b77c87894 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml @@ -0,0 +1,108 @@ +<?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:orientation="vertical" + android:background="@android:color/holo_blue_bright"> + + <!-- All the buttons (and other clickable elements) should be arranged in a way so that it is + possible to "cycle" over all them by clicking on the D-Pad DOWN button. The way we do it + here is by arranging them this vertical LL and by relying on the nextFocusDown attribute + where things are arranged differently and to circle back up to the top once we reach the + bottom. --> + + <Button + android:id="@+id/enter_pip" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Enter PIP" + android:onClick="enterPip"/> + + <CheckBox + android:id="@+id/with_custom_actions" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="With custom actions"/> + + <RadioGroup + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:checkedButton="@id/ratio_default"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Ratio"/> + + <RadioButton + android:id="@+id/ratio_default" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Default" + android:onClick="onRatioSelected"/> + + <RadioButton + android:id="@+id/ratio_square" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Square [1:1]" + android:onClick="onRatioSelected"/> + + <RadioButton + android:id="@+id/ratio_wide" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Wide [2:1]" + android:onClick="onRatioSelected"/> + + <RadioButton + android:id="@+id/ratio_tall" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Tall [1:2]" + android:onClick="onRatioSelected"/> + </RadioGroup> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Media Session"/> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <Button + android:id="@+id/media_session_start" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:nextFocusDown="@id/media_session_stop" + android:text="Start"/> + + <Button + android:id="@+id/media_session_stop" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:nextFocusDown="@id/enter_pip" + android:text="Stop"/> + + </LinearLayout> + +</LinearLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_simple.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_simple.xml new file mode 100644 index 000000000000..5d94e5177dcc --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_simple.xml @@ -0,0 +1,23 @@ +<?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:background="@android:color/holo_orange_light"> + +</LinearLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen.xml new file mode 100644 index 000000000000..84789f5a6c02 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen.xml @@ -0,0 +1,32 @@ +<?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:orientation="vertical" + android:background="@android:color/holo_green_light"> + + <TextView + android:id="@+id/SplitScreenTest" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:gravity="center_vertical|center_horizontal" + android:text="PrimaryActivity" + android:textAppearance="?android:attr/textAppearanceLarge"/> + +</LinearLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen_secondary.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen_secondary.xml new file mode 100644 index 000000000000..674bb70ad01e --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen_secondary.xml @@ -0,0 +1,32 @@ +<?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:orientation="vertical" + android:background="@android:color/holo_blue_light"> + + <TextView + android:id="@+id/SplitScreenTest" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:gravity="center_vertical|center_horizontal" + android:text="SecondaryActivity" + android:textAppearance="?android:attr/textAppearanceLarge"/> + +</LinearLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java new file mode 100644 index 000000000000..0ead91bb37de --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java @@ -0,0 +1,90 @@ +/* + * 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.content.ComponentName; + +public class Components { + public static final String PACKAGE_NAME = "com.android.wm.shell.flicker.testapp"; + + public static class SimpleActivity { + public static final String LABEL = "SimpleApp"; + public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, + PACKAGE_NAME + ".SimpleActivity"); + } + + public static class FixedActivity { + public static final String EXTRA_FIXED_ORIENTATION = "fixed_orientation"; + public static final String LABEL = "FixedApp"; + public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, + PACKAGE_NAME + ".FixedActivity"); + } + + public static class NonResizeableActivity { + public static final String LABEL = "NonResizeableApp"; + public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, + PACKAGE_NAME + ".NonResizeableActivity"); + } + + public static class PipActivity { + // Test App > Pip Activity + public static final String LABEL = "PipApp"; + public static final String MENU_ACTION_NO_OP = "No-Op"; + public static final String MENU_ACTION_ON = "On"; + public static final String MENU_ACTION_OFF = "Off"; + public static final String MENU_ACTION_CLEAR = "Clear"; + + // Intent action that this activity dynamically registers to enter picture-in-picture + public static final String ACTION_ENTER_PIP = PACKAGE_NAME + ".PipActivity.ENTER_PIP"; + // Intent action that this activity dynamically registers to set requested orientation. + // Will apply the oriention to the value set in the EXTRA_FIXED_ORIENTATION extra. + public static final String ACTION_SET_REQUESTED_ORIENTATION = + PACKAGE_NAME + ".PipActivity.SET_REQUESTED_ORIENTATION"; + + // Calls enterPictureInPicture() on creation + public static final String EXTRA_ENTER_PIP = "enter_pip"; + // Sets the fixed orientation (can be one of {@link ActivityInfo.ScreenOrientation} + public static final String EXTRA_PIP_ORIENTATION = "fixed_orientation"; + // Adds a click listener to finish this activity when it is clicked + public static final String EXTRA_TAP_TO_FINISH = "tap_to_finish"; + + public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, + PACKAGE_NAME + ".PipActivity"); + } + + public static class ImeActivity { + public static final String LABEL = "ImeApp"; + public static final String ACTION_CLOSE_IME = + PACKAGE_NAME + ".action.CLOSE_IME"; + public static final String ACTION_OPEN_IME = + PACKAGE_NAME + ".action.OPEN_IME"; + public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, + PACKAGE_NAME + ".ImeActivity"); + } + + public static class SplitScreenActivity { + public static final String LABEL = "SplitScreenPrimaryApp"; + public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, + PACKAGE_NAME + ".SplitScreenActivity"); + } + + public static class SplitScreenSecondaryActivity { + public static final String LABEL = "SplitScreenSecondaryApp"; + public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, + PACKAGE_NAME + ".SplitScreenSecondaryActivity"); + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/FixedActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/FixedActivity.java new file mode 100644 index 000000000000..d4ae6c1313bf --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/FixedActivity.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.flicker.testapp; + +import static com.android.wm.shell.flicker.testapp.Components.FixedActivity.EXTRA_FIXED_ORIENTATION; + +import android.os.Bundle; + +public class FixedActivity extends SimpleActivity { + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + // Set the fixed orientation if requested + if (getIntent().hasExtra(EXTRA_FIXED_ORIENTATION)) { + final int ori = Integer.parseInt(getIntent().getStringExtra(EXTRA_FIXED_ORIENTATION)); + setRequestedOrientation(ori); + } + } +} 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..59c64a1345ab --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/ImeActivity.java @@ -0,0 +1,66 @@ +/* + * 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.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; + +public class ImeActivity extends Activity { + private static final String ACTION_OPEN_IME = + "com.android.wm.shell.flicker.testapp.action.OPEN_IME"; + private static final String ACTION_CLOSE_IME = + "com.android.wm.shell.flicker.testapp.action.CLOSE_IME"; + + private InputMethodManager mImm; + private View mEditText; + + @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); + + mEditText = findViewById(R.id.plain_text_input); + mImm = getSystemService(InputMethodManager.class); + + handleIntent(getIntent()); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + handleIntent(intent); + } + + private void handleIntent(Intent intent) { + final String action = intent.getAction(); + if (ACTION_OPEN_IME.equals(action)) { + mEditText.requestFocus(); + mImm.showSoftInput(mEditText, InputMethodManager.SHOW_FORCED); + } else if (ACTION_CLOSE_IME.equals(action)) { + mImm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0); + mEditText.clearFocus(); + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/NonResizeableActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/NonResizeableActivity.java new file mode 100644 index 000000000000..24275e002c7f --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/NonResizeableActivity.java @@ -0,0 +1,29 @@ +/* + * 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.os.Bundle; + +public class NonResizeableActivity extends Activity { + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.activity_non_resizeable); + } +} 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..a6ba7823e22d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java @@ -0,0 +1,281 @@ +/* + * 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 static android.media.MediaMetadata.METADATA_KEY_TITLE; +import static android.media.session.PlaybackState.ACTION_PAUSE; +import static android.media.session.PlaybackState.ACTION_PLAY; +import static android.media.session.PlaybackState.ACTION_STOP; +import static android.media.session.PlaybackState.STATE_PAUSED; +import static android.media.session.PlaybackState.STATE_PLAYING; +import static android.media.session.PlaybackState.STATE_STOPPED; + +import static com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_ENTER_PIP; +import static com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_SET_REQUESTED_ORIENTATION; +import static com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP; +import static com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_PIP_ORIENTATION; + +import android.app.Activity; +import android.app.PendingIntent; +import android.app.PictureInPictureParams; +import android.app.RemoteAction; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.drawable.Icon; +import android.media.MediaMetadata; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.os.Bundle; +import android.util.Log; +import android.util.Rational; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.CheckBox; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class PipActivity extends Activity { + private static final String TAG = PipActivity.class.getSimpleName(); + /** + * A media session title for when the session is in {@link STATE_PLAYING}. + * TvPipNotificationTests check whether the actual notification title matches this string. + */ + private static final String TITLE_STATE_PLAYING = "TestApp media is playing"; + /** + * A media session title for when the session is in {@link STATE_PAUSED}. + * TvPipNotificationTests check whether the actual notification title matches this string. + */ + private static final String TITLE_STATE_PAUSED = "TestApp media is paused"; + + private static final Rational RATIO_DEFAULT = null; + private static final Rational RATIO_SQUARE = new Rational(1, 1); + private static final Rational RATIO_WIDE = new Rational(2, 1); + private static final Rational RATIO_TALL = new Rational(1, 2); + + private static final String PIP_ACTION_NO_OP = "No-Op"; + private static final String PIP_ACTION_OFF = "Off"; + private static final String PIP_ACTION_ON = "On"; + private static final String PIP_ACTION_CLEAR = "Clear"; + private static final String ACTION_NO_OP = "com.android.wm.shell.flicker.testapp.NO_OP"; + private static final String ACTION_SWITCH_OFF = + "com.android.wm.shell.flicker.testapp.SWITCH_OFF"; + private static final String ACTION_SWITCH_ON = "com.android.wm.shell.flicker.testapp.SWITCH_ON"; + private static final String ACTION_CLEAR = "com.android.wm.shell.flicker.testapp.CLEAR"; + + private final PictureInPictureParams.Builder mPipParamsBuilder = + new PictureInPictureParams.Builder() + .setAspectRatio(RATIO_DEFAULT); + private MediaSession mMediaSession; + private final PlaybackState.Builder mPlaybackStateBuilder = new PlaybackState.Builder() + .setActions(ACTION_PLAY | ACTION_PAUSE | ACTION_STOP) + .setState(STATE_STOPPED, 0, 1f); + private PlaybackState mPlaybackState = mPlaybackStateBuilder.build(); + private final MediaMetadata.Builder mMediaMetadataBuilder = new MediaMetadata.Builder(); + + private final List<RemoteAction> mSwitchOffActions = new ArrayList<>(); + private final List<RemoteAction> mSwitchOnActions = new ArrayList<>(); + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (isInPictureInPictureMode()) { + switch (intent.getAction()) { + case ACTION_SWITCH_ON: + mPipParamsBuilder.setActions(mSwitchOnActions); + break; + case ACTION_SWITCH_OFF: + mPipParamsBuilder.setActions(mSwitchOffActions); + break; + case ACTION_CLEAR: + mPipParamsBuilder.setActions(Collections.emptyList()); + break; + case ACTION_NO_OP: + return; + default: + Log.w(TAG, "Unhandled action=" + intent.getAction()); + return; + } + setPictureInPictureParams(mPipParamsBuilder.build()); + } else { + switch (intent.getAction()) { + case ACTION_ENTER_PIP: + enterPip(null); + break; + case ACTION_SET_REQUESTED_ORIENTATION: + setRequestedOrientation(Integer.parseInt(intent.getStringExtra( + EXTRA_PIP_ORIENTATION))); + break; + default: + Log.w(TAG, "Unhandled action=" + intent.getAction()); + return; + } + } + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final Window window = getWindow(); + final WindowManager.LayoutParams layoutParams = window.getAttributes(); + layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams + .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + window.setAttributes(layoutParams); + + setContentView(R.layout.activity_pip); + + findViewById(R.id.media_session_start) + .setOnClickListener(v -> updateMediaSessionState(STATE_PLAYING)); + findViewById(R.id.media_session_stop) + .setOnClickListener(v -> updateMediaSessionState(STATE_STOPPED)); + + mMediaSession = new MediaSession(this, "WMShell_TestApp"); + mMediaSession.setPlaybackState(mPlaybackStateBuilder.build()); + mMediaSession.setCallback(new MediaSession.Callback() { + @Override + public void onPlay() { + updateMediaSessionState(STATE_PLAYING); + } + + @Override + public void onPause() { + updateMediaSessionState(STATE_PAUSED); + } + + @Override + public void onStop() { + updateMediaSessionState(STATE_STOPPED); + } + }); + + // Build two sets of the custom actions. We'll replace one with the other when 'On'/'Off' + // action is invoked. + // The first set consists of 3 actions: 1) Off; 2) No-Op; 3) Clear. + // The second set consists of 2 actions: 1) On; 2) Clear. + // Upon invocation 'Clear' action clear-off all the custom actions, including itself. + final Icon icon = Icon.createWithResource(this, android.R.drawable.ic_menu_help); + final RemoteAction noOpAction = buildRemoteAction(icon, PIP_ACTION_NO_OP, ACTION_NO_OP); + final RemoteAction switchOnAction = + buildRemoteAction(icon, PIP_ACTION_ON, ACTION_SWITCH_ON); + final RemoteAction switchOffAction = + buildRemoteAction(icon, PIP_ACTION_OFF, ACTION_SWITCH_OFF); + final RemoteAction clearAllAction = buildRemoteAction(icon, PIP_ACTION_CLEAR, ACTION_CLEAR); + mSwitchOffActions.addAll(Arrays.asList(switchOnAction, clearAllAction)); + mSwitchOnActions.addAll(Arrays.asList(noOpAction, switchOffAction, clearAllAction)); + + final IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_NO_OP); + filter.addAction(ACTION_SWITCH_ON); + filter.addAction(ACTION_SWITCH_OFF); + filter.addAction(ACTION_CLEAR); + filter.addAction(ACTION_SET_REQUESTED_ORIENTATION); + filter.addAction(ACTION_ENTER_PIP); + registerReceiver(mBroadcastReceiver, filter); + + handleIntentExtra(getIntent()); + } + + @Override + protected void onDestroy() { + unregisterReceiver(mBroadcastReceiver); + super.onDestroy(); + } + + private RemoteAction buildRemoteAction(Icon icon, String label, String action) { + final Intent intent = new Intent(action); + final PendingIntent pendingIntent = + PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); + return new RemoteAction(icon, label, label, pendingIntent); + } + + public void enterPip(View v) { + final boolean withCustomActions = + ((CheckBox) findViewById(R.id.with_custom_actions)).isChecked(); + mPipParamsBuilder.setActions( + withCustomActions ? mSwitchOnActions : Collections.emptyList()); + enterPictureInPictureMode(mPipParamsBuilder.build()); + } + + public void onRatioSelected(View v) { + switch (v.getId()) { + case R.id.ratio_default: + mPipParamsBuilder.setAspectRatio(RATIO_DEFAULT); + break; + + case R.id.ratio_square: + mPipParamsBuilder.setAspectRatio(RATIO_SQUARE); + break; + + case R.id.ratio_wide: + mPipParamsBuilder.setAspectRatio(RATIO_WIDE); + break; + + case R.id.ratio_tall: + mPipParamsBuilder.setAspectRatio(RATIO_TALL); + break; + } + } + + private void updateMediaSessionState(int newState) { + if (mPlaybackState.getState() == newState) { + return; + } + final String title; + switch (newState) { + case STATE_PLAYING: + title = TITLE_STATE_PLAYING; + break; + case STATE_PAUSED: + title = TITLE_STATE_PAUSED; + break; + case STATE_STOPPED: + title = ""; + break; + + default: + throw new IllegalArgumentException("Unknown state " + newState); + } + + mPlaybackStateBuilder.setState(newState, 0, 1f); + mPlaybackState = mPlaybackStateBuilder.build(); + + mMediaMetadataBuilder.putText(METADATA_KEY_TITLE, title); + + mMediaSession.setPlaybackState(mPlaybackState); + mMediaSession.setMetadata(mMediaMetadataBuilder.build()); + mMediaSession.setActive(newState != STATE_STOPPED); + } + + private void handleIntentExtra(Intent intent) { + // Set the fixed orientation if requested + if (intent.hasExtra(EXTRA_PIP_ORIENTATION)) { + final int ori = Integer.parseInt(getIntent().getStringExtra(EXTRA_PIP_ORIENTATION)); + setRequestedOrientation(ori); + } + // Enter picture in picture with the given aspect ratio if provided + if (intent.hasExtra(EXTRA_ENTER_PIP)) { + mPipParamsBuilder.setActions(mSwitchOnActions); + enterPip(null); + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SimpleActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SimpleActivity.java new file mode 100644 index 000000000000..5343c1893d4e --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SimpleActivity.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 SimpleActivity 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_simple); + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SplitScreenActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SplitScreenActivity.java new file mode 100644 index 000000000000..9c82eea1e8b8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SplitScreenActivity.java @@ -0,0 +1,29 @@ +/* + * 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.os.Bundle; + +public class SplitScreenActivity extends Activity { + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.activity_splitscreen); + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SplitScreenSecondaryActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SplitScreenSecondaryActivity.java new file mode 100644 index 000000000000..baa1e6fdd1e9 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SplitScreenSecondaryActivity.java @@ -0,0 +1,28 @@ +/* + * 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.os.Bundle; + +public class SplitScreenSecondaryActivity extends Activity { + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.activity_splitscreen_secondary); + } +} diff --git a/libs/WindowManager/Shell/tests/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp index 78fa45ebdf94..dca27328f192 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,30 @@ 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", + "platform-test-annotations", ], + 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..59d9104fb5ba 100644 --- a/libs/WindowManager/Shell/tests/AndroidManifest.xml +++ b/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml @@ -22,6 +22,13 @@ <application android:debuggable="true" android:largeHeap="true"> <uses-library android:name="android.test.mock" /> <uses-library android:name="android.test.runner" /> + + <activity android:name=".bubbles.BubblesTestActivity" + android:allowEmbedded="true" + android:documentLaunchMode="always" + android:excludeFromRecents="true" + android:exported="false" + android:resizeableActivity="true" /> </application> <instrumentation 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/unittest/res/layout/main.xml b/libs/WindowManager/Shell/tests/unittest/res/layout/main.xml new file mode 100644 index 000000000000..0d09f868126e --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/res/layout/main.xml @@ -0,0 +1,32 @@ +<?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:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + > + <TextView + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:text="this is a test activity" + /> + <EditText + android:layout_height="wrap_content" + android:id="@+id/editText1" + android:layout_width="match_parent"> + <requestFocus></requestFocus> + </EditText> +</LinearLayout> 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..80ea9b9e177e --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java @@ -0,0 +1,310 @@ +/* + * 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_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; +import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN; +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 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.Context; +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 com.android.wm.shell.sizecompatui.SizeCompatUI; + +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. + * + * Build/Install/Run: + * atest WMShellUnitTests:ShellTaskOrganizerTests + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ShellTaskOrganizerTests { + + @Mock + private ITaskOrganizerController mTaskOrganizerController; + @Mock + private Context mContext; + @Mock + private SizeCompatUI mSizeCompatUI; + + 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, mTestExecutor, mContext, + mSizeCompatUI)); + } + + @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); + } + + @Test + public void testGetParentTaskListener() { + RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + TrackingTaskListener mwListener = new TrackingTaskListener(); + mOrganizer.onTaskAppeared(task1, null); + mOrganizer.addListenerForTaskId(mwListener, task1.taskId); + RunningTaskInfo task2 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); + task2.parentTaskId = task1.taskId; + + mOrganizer.onTaskAppeared(task2, null); + + assertTrue(mwListener.appeared.contains(task2)); + } + + @Test + public void testOnSizeCompatActivityChanged() { + final RunningTaskInfo taskInfo1 = createTaskInfo(12, WINDOWING_MODE_FULLSCREEN); + taskInfo1.displayId = DEFAULT_DISPLAY; + taskInfo1.topActivityToken = mock(IBinder.class); + taskInfo1.topActivityInSizeCompat = false; + final TrackingTaskListener taskListener = new TrackingTaskListener(); + mOrganizer.addListenerForType(taskListener, TASK_LISTENER_TYPE_FULLSCREEN); + mOrganizer.onTaskAppeared(taskInfo1, null); + + // sizeCompatActivity is null if top activity is not in size compat. + verify(mSizeCompatUI).onSizeCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, + taskInfo1.configuration.windowConfiguration.getBounds(), + null /* sizeCompatActivity*/ , taskListener); + + // sizeCompatActivity is non-null if top activity is in size compat. + final RunningTaskInfo taskInfo2 = + createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); + taskInfo2.displayId = taskInfo1.displayId; + taskInfo2.topActivityToken = taskInfo1.topActivityToken; + taskInfo2.topActivityInSizeCompat = true; + mOrganizer.onTaskInfoChanged(taskInfo2); + verify(mSizeCompatUI).onSizeCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, + taskInfo1.configuration.windowConfiguration.getBounds(), + taskInfo1.topActivityToken, + taskListener); + + mOrganizer.onTaskVanished(taskInfo1); + verify(mSizeCompatUI).onSizeCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, + null /* taskConfig */, null /* sizeCompatActivity*/, + null /* taskListener */); + } + + private static 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/ShellTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java new file mode 100644 index 000000000000..5bdf831a81f4 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.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; + +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.After; +import org.junit.Before; + +/** + * Base class that does shell test case setup. + */ +public abstract class ShellTestCase { + + protected TestableContext mContext; + + @Before + public void shellSetup() { + 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(); + } + + @After + public void shellTearDown() { + InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .dropShellPermissionIdentity(); + } + + protected Context getContext() { + return mContext; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java new file mode 100644 index 000000000000..4e582c242094 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell; + +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.PendingIntent; +import android.content.Context; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.SurfaceControl; +import android.view.SurfaceHolder; +import android.view.SurfaceSession; +import android.window.WindowContainerToken; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.HandlerExecutor; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class TaskViewTest extends ShellTestCase { + + @Mock + TaskView.Listener mViewListener; + @Mock + ActivityManager.RunningTaskInfo mTaskInfo; + @Mock + WindowContainerToken mToken; + @Mock + ShellTaskOrganizer mOrganizer; + @Mock + HandlerExecutor mExecutor; + + SurfaceSession mSession; + SurfaceControl mLeash; + + Context mContext; + TaskView mTaskView; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mLeash = new SurfaceControl.Builder(mSession) + .setName("test") + .build(); + + mContext = getContext(); + + mTaskInfo = new ActivityManager.RunningTaskInfo(); + mTaskInfo.token = mToken; + mTaskInfo.taskId = 314; + mTaskInfo.taskDescription = mock(ActivityManager.TaskDescription.class); + + doAnswer((InvocationOnMock invocationOnMock) -> { + final Runnable r = invocationOnMock.getArgument(0); + r.run(); + return null; + }).when(mExecutor).execute(any()); + + when(mOrganizer.getExecutor()).thenReturn(mExecutor); + mTaskView = new TaskView(mContext, mOrganizer); + mTaskView.setListener(mExecutor, mViewListener); + } + + @After + public void tearDown() { + if (mTaskView != null) { + mTaskView.release(); + } + } + + @Test + public void testSetPendingListener_throwsException() { + TaskView taskView = new TaskView(mContext, mOrganizer); + taskView.setListener(mExecutor, mViewListener); + try { + taskView.setListener(mExecutor, mViewListener); + } catch (IllegalStateException e) { + // pass + return; + } + fail("Expected IllegalStateException"); + } + + @Test + public void testStartActivity() { + ActivityOptions options = ActivityOptions.makeBasic(); + mTaskView.startActivity(mock(PendingIntent.class), null, options); + + verify(mOrganizer).setPendingLaunchCookieListener(any(), eq(mTaskView)); + assertThat(options.getLaunchWindowingMode()).isEqualTo(WINDOWING_MODE_MULTI_WINDOW); + } + + @Test + public void testOnTaskAppeared_noSurface() { + mTaskView.onTaskAppeared(mTaskInfo, mLeash); + + verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any()); + verify(mViewListener, never()).onInitialized(); + // If there's no surface the task should be made invisible + verify(mViewListener).onTaskVisibilityChanged(eq(mTaskInfo.taskId), eq(false)); + } + + @Test + public void testOnTaskAppeared_withSurface() { + mTaskView.surfaceCreated(mock(SurfaceHolder.class)); + mTaskView.onTaskAppeared(mTaskInfo, mLeash); + + verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any()); + verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean()); + } + + @Test + public void testSurfaceCreated_noTask() { + mTaskView.surfaceCreated(mock(SurfaceHolder.class)); + + verify(mViewListener).onInitialized(); + // No task, no visibility change + verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean()); + } + + @Test + public void testSurfaceCreated_withTask() { + mTaskView.onTaskAppeared(mTaskInfo, mLeash); + mTaskView.surfaceCreated(mock(SurfaceHolder.class)); + + verify(mViewListener).onInitialized(); + verify(mViewListener).onTaskVisibilityChanged(eq(mTaskInfo.taskId), eq(true)); + } + + @Test + public void testSurfaceDestroyed_noTask() { + SurfaceHolder sh = mock(SurfaceHolder.class); + mTaskView.surfaceCreated(sh); + mTaskView.surfaceDestroyed(sh); + + verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean()); + } + + @Test + public void testSurfaceDestroyed_withTask() { + SurfaceHolder sh = mock(SurfaceHolder.class); + mTaskView.onTaskAppeared(mTaskInfo, mLeash); + mTaskView.surfaceCreated(sh); + reset(mViewListener); + mTaskView.surfaceDestroyed(sh); + + verify(mViewListener).onTaskVisibilityChanged(eq(mTaskInfo.taskId), eq(false)); + } + + @Test + public void testOnReleased() { + mTaskView.onTaskAppeared(mTaskInfo, mLeash); + mTaskView.surfaceCreated(mock(SurfaceHolder.class)); + mTaskView.release(); + + verify(mOrganizer).removeListener(eq(mTaskView)); + verify(mViewListener).onReleased(); + } + + @Test + public void testOnTaskVanished() { + mTaskView.onTaskAppeared(mTaskInfo, mLeash); + mTaskView.surfaceCreated(mock(SurfaceHolder.class)); + mTaskView.onTaskVanished(mTaskInfo); + + verify(mViewListener).onTaskRemovalStarted(eq(mTaskInfo.taskId)); + } + + @Test + public void testOnBackPressedOnTaskRoot() { + mTaskView.onTaskAppeared(mTaskInfo, mLeash); + mTaskView.onBackPressedOnTaskRoot(mTaskInfo); + + verify(mViewListener).onBackPressedOnTaskRoot(eq(mTaskInfo.taskId)); + } + + @Test + public void testSetOnBackPressedOnTaskRoot() { + mTaskView.onTaskAppeared(mTaskInfo, mLeash); + verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(true)); + } + + @Test + public void testUnsetOnBackPressedOnTaskRoot() { + mTaskView.onTaskAppeared(mTaskInfo, mLeash); + verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(true)); + + mTaskView.onTaskVanished(mTaskInfo); + verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(false)); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java new file mode 100644 index 000000000000..1f58a8546796 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.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; + +import static android.app.ActivityTaskManager.INVALID_TASK_ID; + +import android.app.ActivityManager; +import android.graphics.Rect; +import android.window.IWindowContainerToken; +import android.window.WindowContainerToken; + +public final class TestRunningTaskInfoBuilder { + static int sNextTaskId = 500; + private Rect mBounds = new Rect(0, 0, 100, 100); + private WindowContainerToken mToken = + new WindowContainerToken(new IWindowContainerToken.Default()); + private int mParentTaskId = INVALID_TASK_ID; + + public TestRunningTaskInfoBuilder setBounds(Rect bounds) { + mBounds.set(bounds); + return this; + } + + public TestRunningTaskInfoBuilder setParentTaskId(int taskId) { + mParentTaskId = taskId; + return this; + } + + public ActivityManager.RunningTaskInfo build() { + final ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo(); + info.parentTaskId = INVALID_TASK_ID; + info.taskId = sNextTaskId++; + info.parentTaskId = mParentTaskId; + info.configuration.windowConfiguration.setBounds(mBounds); + info.token = mToken; + info.isResizeable = true; + return info; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java new file mode 100644 index 000000000000..bf84a6e30c98 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell; + +import android.os.Looper; + +import com.android.wm.shell.common.ShellExecutor; + +import java.util.ArrayList; + +/** + * Really basic test executor. It just gathers all events in a blob. The only option is to + * execute everything at once. If better control over delayed execution is needed, please add it. + */ +public class TestShellExecutor implements ShellExecutor { + final ArrayList<Runnable> mRunnables = new ArrayList<>(); + + @Override + public void execute(Runnable runnable) { + mRunnables.add(runnable); + } + + @Override + public void executeDelayed(Runnable r, long delayMillis) { + mRunnables.add(r); + } + + @Override + public void removeAllCallbacks() { + mRunnables.clear(); + } + + @Override + public void removeCallbacks(Runnable r) { + mRunnables.remove(r); + } + + @Override + public boolean hasCallback(Runnable r) { + return mRunnables.contains(r); + } + + public void flushAll() { + for (Runnable r : mRunnables) { + r.run(); + } + mRunnables.clear(); + } +} 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/apppairs/AppPairTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java new file mode 100644 index 000000000000..d21183e10ed9 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java @@ -0,0 +1,100 @@ +/* + * 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.apppairs; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.hardware.display.DisplayManager; + +import androidx.test.annotation.UiThreadTest; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestRunningTaskInfoBuilder; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.SyncTransactionQueue; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Tests for {@link AppPair} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class AppPairTests extends ShellTestCase { + + private AppPairsController mController; + @Mock private SyncTransactionQueue mSyncQueue; + @Mock private ShellTaskOrganizer mTaskOrganizer; + @Mock private DisplayController mDisplayController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext); + when(mDisplayController.getDisplay(anyInt())).thenReturn( + mContext.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY)); + mController = new TestAppPairsController( + mTaskOrganizer, + mSyncQueue, + mDisplayController); + } + + @After + public void tearDown() {} + + @Test + @UiThreadTest + public void testContains() { + final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build(); + final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build(); + + final AppPair pair = mController.pairInner(task1, task2); + assertThat(pair.contains(task1.taskId)).isTrue(); + assertThat(pair.contains(task2.taskId)).isTrue(); + + pair.unpair(); + assertThat(pair.contains(task1.taskId)).isFalse(); + assertThat(pair.contains(task2.taskId)).isFalse(); + } + + @Test + @UiThreadTest + public void testVanishUnpairs() { + final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build(); + final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build(); + + final AppPair pair = mController.pairInner(task1, task2); + assertThat(pair.contains(task1.taskId)).isTrue(); + assertThat(pair.contains(task2.taskId)).isTrue(); + + pair.onTaskVanished(task1); + assertThat(pair.contains(task1.taskId)).isFalse(); + assertThat(pair.contains(task2.taskId)).isFalse(); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java new file mode 100644 index 000000000000..505c153eff9c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.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.apppairs; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.hardware.display.DisplayManager; + +import androidx.test.annotation.UiThreadTest; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestRunningTaskInfoBuilder; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.SyncTransactionQueue; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Tests for {@link AppPairsController} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class AppPairsControllerTests extends ShellTestCase { + private TestAppPairsController mController; + private TestAppPairsPool mPool; + @Mock private SyncTransactionQueue mSyncQueue; + @Mock private ShellTaskOrganizer mTaskOrganizer; + @Mock private DisplayController mDisplayController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext); + when(mDisplayController.getDisplay(anyInt())).thenReturn( + mContext.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY)); + mController = new TestAppPairsController( + mTaskOrganizer, + mSyncQueue, + mDisplayController); + mPool = mController.getPool(); + } + + @After + public void tearDown() {} + + @Test + @UiThreadTest + public void testPairUnpair() { + final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build(); + final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build(); + + final AppPair pair = mController.pairInner(task1, task2); + assertThat(pair.contains(task1.taskId)).isTrue(); + assertThat(pair.contains(task2.taskId)).isTrue(); + assertThat(mPool.poolSize()).isGreaterThan(0); + + mController.unpair(task2.taskId); + assertThat(pair.contains(task1.taskId)).isFalse(); + assertThat(pair.contains(task2.taskId)).isFalse(); + assertThat(mPool.poolSize()).isGreaterThan(1); + } + + @Test + @UiThreadTest + public void testUnpair_DontReleaseToPool() { + final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build(); + final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build(); + + final AppPair pair = mController.pairInner(task1, task2); + assertThat(pair.contains(task1.taskId)).isTrue(); + assertThat(pair.contains(task2.taskId)).isTrue(); + + mController.unpair(task2.taskId, false /* releaseToPool */); + assertThat(pair.contains(task1.taskId)).isFalse(); + assertThat(pair.contains(task2.taskId)).isFalse(); + assertThat(mPool.poolSize()).isEqualTo(1); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsPoolTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsPoolTests.java new file mode 100644 index 000000000000..a3f134ee97ed --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsPoolTests.java @@ -0,0 +1,77 @@ +/* + * 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.apppairs; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.SyncTransactionQueue; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Tests for {@link AppPairsPool} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class AppPairsPoolTests extends ShellTestCase { + private TestAppPairsController mController; + private TestAppPairsPool mPool; + @Mock private SyncTransactionQueue mSyncQueue; + @Mock private ShellTaskOrganizer mTaskOrganizer; + @Mock private DisplayController mDisplayController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext); + mController = new TestAppPairsController( + mTaskOrganizer, + mSyncQueue, + mDisplayController); + mPool = mController.getPool(); + } + + @After + public void tearDown() {} + + @Test + public void testInitialState() { + // Pool should always start off with at least 1 entry. + assertThat(mPool.poolSize()).isGreaterThan(0); + } + + @Test + public void testAcquireRelease() { + assertThat(mPool.poolSize()).isGreaterThan(0); + final AppPair appPair = mPool.acquire(); + assertThat(mPool.poolSize()).isGreaterThan(0); + mPool.release(appPair); + assertThat(mPool.poolSize()).isGreaterThan(1); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java new file mode 100644 index 000000000000..e094158e1144 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java @@ -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. + */ + +package com.android.wm.shell.apppairs; + +import static org.mockito.Mockito.mock; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; + +import org.mockito.Mock; + +public class TestAppPairsController extends AppPairsController { + private TestAppPairsPool mPool; + + public TestAppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue, + DisplayController displayController) { + super(organizer, syncQueue, displayController, mock(ShellExecutor.class)); + mPool = new TestAppPairsPool(this); + setPairsPool(mPool); + } + + TestAppPairsPool getPool() { + return mPool; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsPool.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsPool.java new file mode 100644 index 000000000000..1ee7fff44892 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsPool.java @@ -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.apppairs; + +import android.app.ActivityManager; + +import com.android.wm.shell.TestRunningTaskInfoBuilder; + +public class TestAppPairsPool extends AppPairsPool{ + TestAppPairsPool(AppPairsController controller) { + super(controller); + } + + @Override + void incrementPool() { + final AppPair entry = new AppPair(mController); + final ActivityManager.RunningTaskInfo info = + new TestRunningTaskInfoBuilder().build(); + entry.onTaskAppeared(info, null /* leash */); + release(entry); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java new file mode 100644 index 000000000000..d3a736e9153e --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -0,0 +1,922 @@ +/* + * 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.bubbles; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.app.Notification; +import android.app.PendingIntent; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.os.UserHandle; +import android.service.notification.NotificationListenerService; +import android.service.notification.StatusBarNotification; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.util.Pair; +import android.view.WindowManager; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.bubbles.BubbleData.TimeSource; +import com.android.wm.shell.common.ShellExecutor; + +import com.google.common.collect.ImmutableList; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests operations and the resulting state managed by BubbleData. + * <p> + * After each operation to verify, {@link #verifyUpdateReceived()} ensures the listener was called + * and captures the Update object received there. + * <p> + * Other methods beginning with 'assert' access the captured update object and assert on specific + * aspects of it. + */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class BubbleDataTest extends ShellTestCase { + + private BubbleEntry mEntryA1; + private BubbleEntry mEntryA2; + private BubbleEntry mEntryA3; + private BubbleEntry mEntryB1; + private BubbleEntry mEntryB2; + private BubbleEntry mEntryB3; + private BubbleEntry mEntryC1; + private BubbleEntry mEntryInterruptive; + private BubbleEntry mEntryDismissed; + + private Bubble mBubbleA1; + private Bubble mBubbleA2; + private Bubble mBubbleA3; + private Bubble mBubbleB1; + private Bubble mBubbleB2; + private Bubble mBubbleB3; + private Bubble mBubbleC1; + private Bubble mBubbleInterruptive; + private Bubble mBubbleDismissed; + + private BubbleData mBubbleData; + + @Mock + private TimeSource mTimeSource; + @Mock + private BubbleData.Listener mListener; + @Mock + private PendingIntent mExpandIntent; + @Mock + private PendingIntent mDeleteIntent; + @Mock + private BubbleLogger mBubbleLogger; + @Mock + private ShellExecutor mMainExecutor; + + @Captor + private ArgumentCaptor<BubbleData.Update> mUpdateCaptor; + + @Mock + private Bubbles.NotificationSuppressionChangedListener mSuppressionListener; + + @Mock + private Bubbles.PendingIntentCanceledListener mPendingIntentCanceledListener; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mEntryA1 = createBubbleEntry(1, "a1", "package.a", null); + mEntryA2 = createBubbleEntry(1, "a2", "package.a", null); + mEntryA3 = createBubbleEntry(1, "a3", "package.a", null); + mEntryB1 = createBubbleEntry(1, "b1", "package.b", null); + mEntryB2 = createBubbleEntry(1, "b2", "package.b", null); + mEntryB3 = createBubbleEntry(1, "b3", "package.b", null); + mEntryC1 = createBubbleEntry(1, "c1", "package.c", null); + + NotificationListenerService.Ranking ranking = + mock(NotificationListenerService.Ranking.class); + when(ranking.visuallyInterruptive()).thenReturn(true); + mEntryInterruptive = createBubbleEntry(1, "interruptive", "package.d", ranking); + mBubbleInterruptive = new Bubble(mEntryInterruptive, mSuppressionListener, null, + mMainExecutor); + + mEntryDismissed = createBubbleEntry(1, "dismissed", "package.d", null); + mBubbleDismissed = new Bubble(mEntryDismissed, mSuppressionListener, null, + mMainExecutor); + + mBubbleA1 = new Bubble(mEntryA1, mSuppressionListener, mPendingIntentCanceledListener, + mMainExecutor); + mBubbleA2 = new Bubble(mEntryA2, mSuppressionListener, mPendingIntentCanceledListener, + mMainExecutor); + mBubbleA3 = new Bubble(mEntryA3, mSuppressionListener, mPendingIntentCanceledListener, + mMainExecutor); + mBubbleB1 = new Bubble(mEntryB1, mSuppressionListener, mPendingIntentCanceledListener, + mMainExecutor); + mBubbleB2 = new Bubble(mEntryB2, mSuppressionListener, mPendingIntentCanceledListener, + mMainExecutor); + mBubbleB3 = new Bubble(mEntryB3, mSuppressionListener, mPendingIntentCanceledListener, + mMainExecutor); + mBubbleC1 = new Bubble(mEntryC1, mSuppressionListener, mPendingIntentCanceledListener, + mMainExecutor); + TestableBubblePositioner positioner = new TestableBubblePositioner(mContext, + mock(WindowManager.class)); + mBubbleData = new BubbleData(getContext(), mBubbleLogger, positioner, + mMainExecutor); + + // Used by BubbleData to set lastAccessedTime + when(mTimeSource.currentTimeMillis()).thenReturn(1000L); + mBubbleData.setTimeSource(mTimeSource); + + // Assert baseline starting state + assertThat(mBubbleData.hasBubbles()).isFalse(); + assertThat(mBubbleData.isExpanded()).isFalse(); + assertThat(mBubbleData.getSelectedBubble()).isNull(); + } + + @Test + public void testAddBubble() { + // Setup + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryA1, 1000); + + // Verify + verifyUpdateReceived(); + assertBubbleAdded(mBubbleA1); + assertSelectionChangedTo(mBubbleA1); + } + + @Test + public void testRemoveBubble() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryA3, 3000); + mBubbleData.setListener(mListener); + + // Test + mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE); + + // Verify + verifyUpdateReceived(); + assertBubbleRemoved(mBubbleA1, Bubbles.DISMISS_USER_GESTURE); + } + + @Test + public void ifSuppress_hideFlyout() { + // Setup + mBubbleData.setListener(mListener); + + // Test + mBubbleData.notificationEntryUpdated(mBubbleC1, /* suppressFlyout */ true, /* showInShade */ + true); + + // Verify + verifyUpdateReceived(); + BubbleData.Update update = mUpdateCaptor.getValue(); + assertThat(update.addedBubble.showFlyout()).isFalse(); + } + + @Test + public void ifInterruptiveAndNotSuppressed_thenShowFlyout() { + // Setup + mBubbleData.setListener(mListener); + + // Test + mBubbleData.notificationEntryUpdated(mBubbleInterruptive, + false /* suppressFlyout */, true /* showInShade */); + + // Verify + verifyUpdateReceived(); + BubbleData.Update update = mUpdateCaptor.getValue(); + assertThat(update.addedBubble.showFlyout()).isTrue(); + } + + @Test + public void sameUpdate_InShade_thenHideFlyout() { + // Setup + mBubbleData.setListener(mListener); + + // Test + mBubbleData.notificationEntryUpdated(mBubbleC1, false /* suppressFlyout */, + true /* showInShade */); + verifyUpdateReceived(); + + mBubbleData.notificationEntryUpdated(mBubbleC1, false /* suppressFlyout */, + true /* showInShade */); + verifyUpdateReceived(); + + // Verify + BubbleData.Update update = mUpdateCaptor.getValue(); + assertThat(update.updatedBubble.showFlyout()).isFalse(); + } + + @Test + public void sameUpdate_NotInShade_NotVisuallyInterruptive_dontShowFlyout() { + // Setup + mBubbleData.setListener(mListener); + + // Test + mBubbleData.notificationEntryUpdated(mBubbleDismissed, false /* suppressFlyout */, + true /* showInShade */); + verifyUpdateReceived(); + + // Suppress the notif / make it look dismissed + mBubbleDismissed.setSuppressNotification(true); + + mBubbleData.notificationEntryUpdated(mBubbleDismissed, false /* suppressFlyout */, + true /* showInShade */); + verifyUpdateReceived(); + + // Verify + BubbleData.Update update = mUpdateCaptor.getValue(); + assertThat(update.updatedBubble.showFlyout()).isFalse(); + } + + // + // Overflow + // + + /** + * Verifies that when the bubble stack reaches its maximum, the oldest bubble is overflowed. + */ + @Test + public void testOverflow_add_stackAtMaxBubbles_overflowsOldest() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryA3, 3000); + sendUpdatedEntryAtTime(mEntryB1, 4000); + sendUpdatedEntryAtTime(mEntryB2, 5000); + mBubbleData.setListener(mListener); + + sendUpdatedEntryAtTime(mEntryC1, 6000); + verifyUpdateReceived(); + assertBubbleRemoved(mBubbleA1, Bubbles.DISMISS_AGED); + assertOverflowChangedTo(ImmutableList.of(mBubbleA1)); + + Bubble bubbleA1 = mBubbleData.getOrCreateBubble(mEntryA1, null /* persistedBubble */); + bubbleA1.markUpdatedAt(7000L); + mBubbleData.notificationEntryUpdated(bubbleA1, false /* suppressFlyout*/, + true /* showInShade */); + verifyUpdateReceived(); + assertBubbleRemoved(mBubbleA2, Bubbles.DISMISS_AGED); + assertOverflowChangedTo(ImmutableList.of(mBubbleA2)); + } + + /** + * Verifies that once the number of overflowed bubbles reaches its maximum, the oldest + * overflow bubble is removed. + */ + @Test + public void testOverflow_maxReached_bubbleRemoved() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryA3, 3000); + mBubbleData.setListener(mListener); + + mBubbleData.setMaxOverflowBubbles(1); + mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE); + verifyUpdateReceived(); + assertOverflowChangedTo(ImmutableList.of(mBubbleA1)); + + // Overflow max of 1 is reached; A1 is oldest, so it gets removed + mBubbleData.dismissBubbleWithKey(mEntryA2.getKey(), Bubbles.DISMISS_USER_GESTURE); + verifyUpdateReceived(); + assertOverflowChangedTo(ImmutableList.of(mBubbleA2)); + } + + /** + * Verifies that overflow bubbles are canceled on notif entry removal. + */ + @Test + public void testOverflow_notifCanceled_removesOverflowBubble() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryA3, 3000); + sendUpdatedEntryAtTime(mEntryB1, 4000); + sendUpdatedEntryAtTime(mEntryB2, 5000); + sendUpdatedEntryAtTime(mEntryB3, 6000); // [A2, A3, B1, B2, B3], overflow: [A1] + sendUpdatedEntryAtTime(mEntryC1, 7000); // [A3, B1, B2, B3, C1], overflow: [A2, A1] + mBubbleData.setListener(mListener); + + // Test + mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_NOTIF_CANCEL); + verifyUpdateReceived(); + assertOverflowChangedTo(ImmutableList.of(mBubbleA2)); + + // Test + mBubbleData.dismissBubbleWithKey(mEntryA2.getKey(), Bubbles.DISMISS_GROUP_CANCELLED); + verifyUpdateReceived(); + assertOverflowChangedTo(ImmutableList.of()); + } + + // COLLAPSED / ADD + + /** + * Verifies that new bubbles insert to the left when collapsed. + * <p> + * Placement within the list is based on {@link Bubble#getLastActivity()}, descending + * order (with most recent first). + */ + @Test + public void test_collapsed_addBubble() { + // Setup + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryA1, 1000); + verifyUpdateReceived(); + assertOrderNotChanged(); + + sendUpdatedEntryAtTime(mEntryB1, 2000); + verifyUpdateReceived(); + assertOrderChangedTo(mBubbleB1, mBubbleA1); + + sendUpdatedEntryAtTime(mEntryB2, 3000); + verifyUpdateReceived(); + assertOrderChangedTo(mBubbleB2, mBubbleB1, mBubbleA1); + + sendUpdatedEntryAtTime(mEntryA2, 4000); + verifyUpdateReceived(); + assertOrderChangedTo(mBubbleA2, mBubbleB2, mBubbleB1, mBubbleA1); + } + + /** + * Verifies that new bubbles become the selected bubble when they appear when the stack is in + * the collapsed state. + * + * @see #test_collapsed_updateBubble_selectionChanges() + */ + @Test + public void test_collapsed_addBubble_selectionChanges() { + // Setup + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryA1, 1000); + verifyUpdateReceived(); + assertSelectionChangedTo(mBubbleA1); + + sendUpdatedEntryAtTime(mEntryB1, 2000); + verifyUpdateReceived(); + assertSelectionChangedTo(mBubbleB1); + + sendUpdatedEntryAtTime(mEntryB2, 3000); + verifyUpdateReceived(); + assertSelectionChangedTo(mBubbleB2); + + sendUpdatedEntryAtTime(mEntryA2, 4000); + verifyUpdateReceived(); + assertSelectionChangedTo(mBubbleA2); + } + + // COLLAPSED / REMOVE + + /** + * Verifies order of bubbles after a removal. + */ + @Test + public void test_collapsed_removeBubble_sort() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryB2, 3000); + sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, B2, B1, A1] + mBubbleData.setListener(mListener); + + // Test + mBubbleData.dismissBubbleWithKey(mEntryA2.getKey(), Bubbles.DISMISS_USER_GESTURE); + verifyUpdateReceived(); + // TODO: this should fail if things work as I expect them to? + assertOrderChangedTo(mBubbleB2, mBubbleB1, mBubbleA1); + } + + /** + * Verifies that onOrderChanged is not called when a bubble is removed if the removal does not + * cause other bubbles to change position. + */ + @Test + public void test_collapsed_removeOldestBubble_doesNotCallOnOrderChanged() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryB2, 3000); + sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, B2, B1, A1] + mBubbleData.setListener(mListener); + + // Test + mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE); + verifyUpdateReceived(); + assertOrderNotChanged(); + } + + /** + * Verifies that when the selected bubble is removed with the stack in the collapsed state, + * the selection moves to the next most-recently updated bubble. + */ + @Test + public void test_collapsed_removeBubble_selectionChanges() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryB2, 3000); + sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, B2, B1, A1] + mBubbleData.setListener(mListener); + + // Test + mBubbleData.dismissBubbleWithKey(mEntryA2.getKey(), Bubbles.DISMISS_NOTIF_CANCEL); + verifyUpdateReceived(); + assertSelectionChangedTo(mBubbleB2); + } + + // COLLAPSED / UPDATE + + /** + * Verifies that bubble ordering changes with updates while the stack is in the + * collapsed state. + */ + @Test + public void test_collapsed_updateBubble() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryB2, 3000); + sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, B2, B1, A1] + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryB1, 5000); + verifyUpdateReceived(); + assertOrderChangedTo(mBubbleB1, mBubbleA2, mBubbleB2, mBubbleA1); + + sendUpdatedEntryAtTime(mEntryA1, 6000); + verifyUpdateReceived(); + assertOrderChangedTo(mBubbleA1, mBubbleB1, mBubbleA2, mBubbleB2); + } + + /** + * Verifies that selection tracks the most recently updated bubble while in the collapsed state. + */ + @Test + public void test_collapsed_updateBubble_selectionChanges() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryB2, 3000); + sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, B2, B1, A1] + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryB1, 5000); + verifyUpdateReceived(); + assertSelectionChangedTo(mBubbleB1); + + sendUpdatedEntryAtTime(mEntryA1, 6000); + verifyUpdateReceived(); + assertSelectionChangedTo(mBubbleA1); + } + + /** + * Verifies that when a non visually interruptive update occurs, that the selection does not + * change. + */ + @Test + public void test_notVisuallyInterruptive_updateBubble_selectionDoesntChange() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryB2, 3000); + sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, B2, B1, A1] + mBubbleData.setListener(mListener); + + assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA2); + + // Test + sendUpdatedEntryAtTime(mEntryB1, 5000, false /* isVisuallyInterruptive */); + assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA2); + } + + /** + * Verifies that a request to expand the stack has no effect if there are no bubbles. + */ + @Test + public void test_collapsed_expansion_whenEmpty_doesNothing() { + assertThat(mBubbleData.hasBubbles()).isFalse(); + mBubbleData.setListener(mListener); + + changeExpandedStateAtTime(true, 2000L); + verifyZeroInteractions(mListener); + } + + /** + * Verifies that removing the last bubble clears the selected bubble and collapses the stack. + */ + @Test + public void test_collapsed_removeLastBubble_clearsSelectedBubble() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + mBubbleData.setListener(mListener); + + // Test + mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE); + + // Verify the selection was cleared. + verifyUpdateReceived(); + assertThat(mBubbleData.isExpanded()).isFalse(); + assertThat(mBubbleData.getSelectedBubble()).isNull(); + } + + // EXPANDED / ADD / UPDATE + + /** + * Verifies that bubbles are added at the front of the stack. + * <p> + * Placement within the list is based on {@link Bubble#getLastActivity()}, descending + * order (with most recent first). + * + * @see #test_collapsed_addBubble() + */ + @Test + public void test_expanded_addBubble() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryB1, 3000); // [B1, A2, A1] + changeExpandedStateAtTime(true, 4000L); // B1 marked updated at 4000L + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryC1, 4000); + verifyUpdateReceived(); + assertOrderChangedTo(mBubbleC1, mBubbleB1, mBubbleA2, mBubbleA1); + } + + /** + * Verifies that updates to bubbles while expanded do not result in any change to sorting + * of bubbles. + */ + @Test + public void test_expanded_updateBubble_noChanges() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryB1, 3000); + sendUpdatedEntryAtTime(mEntryB2, 4000); // [B2, B1, A2, A1] + changeExpandedStateAtTime(true, 5000L); + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryA1, 4000); + verifyUpdateReceived(); + assertOrderNotChanged(); + } + + /** + * Verifies that updates to bubbles while expanded do not result in any change to selection. + * + * @see #test_collapsed_addBubble_selectionChanges() + */ + @Test + public void test_expanded_updateBubble_noSelectionChanges() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryB1, 3000); + sendUpdatedEntryAtTime(mEntryB2, 4000); // [B2, B1, A2, A1] + changeExpandedStateAtTime(true, 5000L); + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryA1, 6000); + verifyUpdateReceived(); + assertOrderNotChanged(); + + sendUpdatedEntryAtTime(mEntryA2, 7000); + verifyUpdateReceived(); + assertOrderNotChanged(); + + sendUpdatedEntryAtTime(mEntryB1, 8000); + verifyUpdateReceived(); + assertOrderNotChanged(); + } + + // EXPANDED / REMOVE + + /** + * Verifies that removing a bubble while expanded does not result in reordering of bubbles. + * + * @see #test_collapsed_addBubble() + */ + @Test + public void test_expanded_removeBubble() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryA2, 3000); + sendUpdatedEntryAtTime(mEntryB2, 4000); // [B2, A2, B1, A1] + changeExpandedStateAtTime(true, 5000L); + mBubbleData.setListener(mListener); + + // Test + mBubbleData.dismissBubbleWithKey(mEntryB2.getKey(), Bubbles.DISMISS_USER_GESTURE); + verifyUpdateReceived(); + assertOrderChangedTo(mBubbleA2, mBubbleB1, mBubbleA1); + } + + /** + * Verifies that removing the selected bubble while expanded causes another bubble to become + * selected. The replacement selection is the bubble which appears at the same index as the + * previous one, or the previous index if this was the last position. + * + * @see #test_collapsed_addBubble() + */ + @Test + public void test_expanded_removeBubble_selectionChanges_whenSelectedRemoved() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryA2, 3000); + sendUpdatedEntryAtTime(mEntryB2, 4000); + changeExpandedStateAtTime(true, 5000L); + mBubbleData.setSelectedBubble(mBubbleA2); // [B2, A2^, B1, A1] + mBubbleData.setListener(mListener); + + // Test + mBubbleData.dismissBubbleWithKey(mEntryA2.getKey(), Bubbles.DISMISS_USER_GESTURE); + verifyUpdateReceived(); + assertSelectionChangedTo(mBubbleB1); + + mBubbleData.dismissBubbleWithKey(mEntryB1.getKey(), Bubbles.DISMISS_USER_GESTURE); + verifyUpdateReceived(); + assertSelectionChangedTo(mBubbleA1); + } + + @Test + public void test_expandAndCollapse_callsOnExpandedChanged() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + mBubbleData.setListener(mListener); + + // Test + changeExpandedStateAtTime(true, 3000L); + verifyUpdateReceived(); + assertExpandedChangedTo(true); + + changeExpandedStateAtTime(false, 4000L); + verifyUpdateReceived(); + assertExpandedChangedTo(false); + } + + /** + * Verifies that transitions between the collapsed and expanded state maintain sorting and + * grouping rules. + * <p> + * While collapsing, sorting is applied since no sorting happens while expanded. The resulting + * state is the new expanded ordering. This state is saved and restored if possible when next + * expanded. + * <p> + * When the stack transitions to the collapsed state, the selected bubble is brought to the top. + * <p> + * When the stack transitions back to the expanded state, this new order is kept as is. + */ + @Test + public void test_expansionChanges() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryA2, 3000); + sendUpdatedEntryAtTime(mEntryB2, 4000); + changeExpandedStateAtTime(true, 5000L); // [B2=4000, A2=3000, B1=2000, A1=1000] + sendUpdatedEntryAtTime(mEntryB1, 6000); // [B2=4000, A2=3000, B1=6000, A1=1000] + setCurrentTime(7000); + mBubbleData.setSelectedBubble(mBubbleA2); + mBubbleData.setListener(mListener); + assertThat(mBubbleData.getBubbles()).isEqualTo( + ImmutableList.of(mBubbleB2, mBubbleA2, mBubbleB1, mBubbleA1)); + + // Test + + // At this point, B1 has been updated but sorting has not been changed because the + // stack is expanded. When next collapsed, sorting will be applied and saved, just prior + // to moving the selected bubble to the top (first). + // + // In this case, the expected re-expand state will be: [A2^, B1, B2, A1] + // + // collapse -> selected bubble (A2) moves first. + changeExpandedStateAtTime(false, 8000L); + verifyUpdateReceived(); + assertOrderChangedTo(mBubbleA2, mBubbleB1, mBubbleB2, mBubbleA1); + } + + /** + * When a change occurs while collapsed (any update, add, remove), the previous expanded + * order becomes invalidated, the stack is resorted and will reflect that when next expanded. + */ + @Test + public void test_expansionChanges_withUpdatesWhileCollapsed() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryA2, 3000); + sendUpdatedEntryAtTime(mEntryB2, 4000); + changeExpandedStateAtTime(true, 5000L); // [B2=4000, A2=3000, B1=2000, A1=1000] + sendUpdatedEntryAtTime(mEntryB1, 6000); // [B2=4000, A2=3000, B1=6000, A1=1000] + setCurrentTime(7000); + mBubbleData.setSelectedBubble(mBubbleA2); // [B2, A2^, B1, A1] + mBubbleData.setListener(mListener); + + // Test + + // At this point, B1 has been updated but sorting has not been changed because the + // stack is expanded. When next collapsed, sorting will be applied and saved, just prior + // to moving the selected bubble to the top (first). + // + // In this case, the expected re-expand state will be: [A2^, B1, B2, A1] + // + // That state is restored as long as no changes occur (add/remove/update) while in + // the collapsed state. + // + // collapse -> selected bubble (A2) moves first. + changeExpandedStateAtTime(false, 8000L); + verifyUpdateReceived(); + assertOrderChangedTo(mBubbleA2, mBubbleB1, mBubbleB2, mBubbleA1); + + // An update occurs, which causes sorting, and this invalidates the previously saved order. + sendUpdatedEntryAtTime(mEntryA1, 9000); + verifyUpdateReceived(); + assertOrderChangedTo(mBubbleA1, mBubbleA2, mBubbleB1, mBubbleB2); + + // No order changes when expanding because the new sorted order remains. + changeExpandedStateAtTime(true, 10000L); + verifyUpdateReceived(); + assertOrderNotChanged(); + } + + @Test + public void test_expanded_removeLastBubble_collapsesStack() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + changeExpandedStateAtTime(true, 2000); + mBubbleData.setListener(mListener); + + // Test + mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE); + verifyUpdateReceived(); + assertExpandedChangedTo(false); + } + + private void verifyUpdateReceived() { + verify(mListener).applyUpdate(mUpdateCaptor.capture()); + reset(mListener); + } + + private void assertBubbleAdded(Bubble expected) { + BubbleData.Update update = mUpdateCaptor.getValue(); + assertWithMessage("addedBubble").that(update.addedBubble).isEqualTo(expected); + } + + private void assertBubbleRemoved(Bubble expected, @Bubbles.DismissReason int reason) { + BubbleData.Update update = mUpdateCaptor.getValue(); + assertWithMessage("removedBubbles").that(update.removedBubbles) + .isEqualTo(ImmutableList.of(Pair.create(expected, reason))); + } + + private void assertOrderNotChanged() { + BubbleData.Update update = mUpdateCaptor.getValue(); + assertWithMessage("orderChanged").that(update.orderChanged).isFalse(); + } + + private void assertOrderChangedTo(Bubble... order) { + BubbleData.Update update = mUpdateCaptor.getValue(); + assertWithMessage("orderChanged").that(update.orderChanged).isTrue(); + assertWithMessage("bubble order").that(update.bubbles) + .isEqualTo(ImmutableList.copyOf(order)); + } + + private void assertSelectionNotChanged() { + BubbleData.Update update = mUpdateCaptor.getValue(); + assertWithMessage("selectionChanged").that(update.selectionChanged).isFalse(); + } + + private void assertSelectionChangedTo(Bubble bubble) { + BubbleData.Update update = mUpdateCaptor.getValue(); + assertWithMessage("selectionChanged").that(update.selectionChanged).isTrue(); + assertWithMessage("selectedBubble").that(update.selectedBubble).isEqualTo(bubble); + } + + private void assertSelectionCleared() { + BubbleData.Update update = mUpdateCaptor.getValue(); + assertWithMessage("selectionChanged").that(update.selectionChanged).isTrue(); + assertWithMessage("selectedBubble").that(update.selectedBubble).isNull(); + } + + private void assertExpandedChangedTo(boolean expected) { + BubbleData.Update update = mUpdateCaptor.getValue(); + assertWithMessage("expandedChanged").that(update.expandedChanged).isTrue(); + assertWithMessage("expanded").that(update.expanded).isEqualTo(expected); + } + + private void assertOverflowChangedTo(ImmutableList<Bubble> bubbles) { + BubbleData.Update update = mUpdateCaptor.getValue(); + assertThat(update.overflowBubbles).isEqualTo(bubbles); + } + + + private BubbleEntry createBubbleEntry(int userId, String notifKey, String packageName, + NotificationListenerService.Ranking ranking) { + return createBubbleEntry(userId, notifKey, packageName, ranking, 1000); + } + + private void setPostTime(BubbleEntry entry, long postTime) { + when(entry.getStatusBarNotification().getPostTime()).thenReturn(postTime); + } + + /** + * No ExpandableNotificationRow is required to test BubbleData. This setup is all that is + * required for BubbleData functionality and verification. NotificationTestHelper is used only + * as a convenience to create a Notification w/BubbleMetadata. + */ + private BubbleEntry createBubbleEntry(int userId, String notifKey, String packageName, + NotificationListenerService.Ranking ranking, long postTime) { + // BubbleMetadata + Notification.BubbleMetadata bubbleMetadata = new Notification.BubbleMetadata.Builder( + mExpandIntent, Icon.createWithResource("", 0)) + .setDeleteIntent(mDeleteIntent) + .build(); + // Notification -> BubbleMetadata + Notification notification = mock(Notification.class); + notification.setBubbleMetadata(bubbleMetadata); + + // Notification -> extras + notification.extras = new Bundle(); + + // StatusBarNotification + StatusBarNotification sbn = mock(StatusBarNotification.class); + when(sbn.getKey()).thenReturn(notifKey); + when(sbn.getUser()).thenReturn(new UserHandle(userId)); + when(sbn.getPackageName()).thenReturn(packageName); + when(sbn.getPostTime()).thenReturn(postTime); + when(sbn.getNotification()).thenReturn(notification); + + // NotificationEntry -> StatusBarNotification -> Notification -> BubbleMetadata + return new BubbleEntry(sbn, ranking, true, false, false, false); + } + + private void setCurrentTime(long time) { + when(mTimeSource.currentTimeMillis()).thenReturn(time); + } + + private void sendUpdatedEntryAtTime(BubbleEntry entry, long postTime) { + sendUpdatedEntryAtTime(entry, postTime, true /* visuallyInterruptive */); + } + + private void sendUpdatedEntryAtTime(BubbleEntry entry, long postTime, + boolean visuallyInterruptive) { + setPostTime(entry, postTime); + // BubbleController calls this: + Bubble b = mBubbleData.getOrCreateBubble(entry, null /* persistedBubble */); + b.setVisuallyInterruptiveForTest(visuallyInterruptive); + // And then this + mBubbleData.notificationEntryUpdated(b, false /* suppressFlyout*/, + true /* showInShade */); + } + + private void changeExpandedStateAtTime(boolean shouldBeExpanded, long time) { + setCurrentTime(time); + mBubbleData.setExpanded(shouldBeExpanded); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java new file mode 100644 index 000000000000..69d5244e5ac2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java @@ -0,0 +1,117 @@ +/* + * 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.bubbles; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotSame; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.graphics.Color; +import android.graphics.PointF; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.View; +import android.widget.TextView; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTestCase; + +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(setAsMainLooper = true) +public class BubbleFlyoutViewTest extends ShellTestCase { + private BubbleFlyoutView mFlyout; + private TextView mFlyoutText; + private TextView mSenderName; + private float[] mDotCenter = new float[2]; + private Bubble.FlyoutMessage mFlyoutMessage; + @Mock + private BubblePositioner mPositioner; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + when(mPositioner.getBubbleBitmapSize()).thenReturn(40); + when(mPositioner.getBubbleSize()).thenReturn(60); + + mFlyoutMessage = new Bubble.FlyoutMessage(); + mFlyoutMessage.senderName = "Josh"; + mFlyoutMessage.message = "Hello"; + + mFlyout = new BubbleFlyoutView(getContext()); + + mFlyoutText = mFlyout.findViewById(R.id.bubble_flyout_text); + mSenderName = mFlyout.findViewById(R.id.bubble_flyout_name); + mDotCenter[0] = 30; + mDotCenter[1] = 30; + } + + @Test + public void testShowFlyout_isVisible() { + mFlyout.setupFlyoutStartingAsDot( + mFlyoutMessage, + new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter, + false, + mPositioner); + mFlyout.setVisibility(View.VISIBLE); + + assertEquals("Hello", mFlyoutText.getText()); + assertEquals("Josh", mSenderName.getText()); + assertEquals(View.VISIBLE, mFlyout.getVisibility()); + } + + @Test + public void testFlyoutHide_runsCallback() { + Runnable after = mock(Runnable.class); + mFlyout.setupFlyoutStartingAsDot(mFlyoutMessage, + new PointF(100, 100), 500, true, Color.WHITE, null, after, mDotCenter, + false, + mPositioner); + mFlyout.hideFlyout(); + + verify(after).run(); + } + + @Test + public void testSetCollapsePercent() { + mFlyout.setupFlyoutStartingAsDot(mFlyoutMessage, + new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter, + false, + mPositioner); + mFlyout.setVisibility(View.VISIBLE); + + mFlyout.setCollapsePercent(1f); + assertEquals(0f, mFlyoutText.getAlpha(), 0.01f); + assertNotSame(0f, mFlyoutText.getTranslationX()); // Should have moved to collapse. + + mFlyout.setCollapsePercent(0f); + assertEquals(1f, mFlyoutText.getAlpha(), 0.01f); + assertEquals(0f, mFlyoutText.getTranslationX()); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java new file mode 100644 index 000000000000..fc828b30279f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java @@ -0,0 +1,165 @@ +/* + * 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.bubbles; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Intent; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.service.notification.StatusBarNotification; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.ShellExecutor; + +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 BubbleTest extends ShellTestCase { + @Mock + private Notification mNotif; + @Mock + private StatusBarNotification mSbn; + @Mock + private ShellExecutor mMainExecutor; + + private BubbleEntry mBubbleEntry; + private Bundle mExtras; + private Bubble mBubble; + + @Mock + private Bubbles.NotificationSuppressionChangedListener mSuppressionListener; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mExtras = new Bundle(); + mNotif.extras = mExtras; + + Intent target = new Intent(mContext, BubblesTestActivity.class); + Notification.BubbleMetadata metadata = new Notification.BubbleMetadata.Builder( + PendingIntent.getActivity(mContext, 0, target, PendingIntent.FLAG_MUTABLE), + Icon.createWithResource(mContext, R.drawable.bubble_ic_create_bubble)) + .build(); + when(mSbn.getNotification()).thenReturn(mNotif); + when(mNotif.getBubbleMetadata()).thenReturn(metadata); + when(mSbn.getKey()).thenReturn("mock"); + mBubbleEntry = new BubbleEntry(mSbn, null, true, false, false, false); + mBubble = new Bubble(mBubbleEntry, mSuppressionListener, null, mMainExecutor); + } + + @Test + public void testGetUpdateMessage_default() { + final String msg = "Hello there!"; + doReturn(Notification.Style.class).when(mNotif).getNotificationStyle(); + mExtras.putCharSequence(Notification.EXTRA_TEXT, msg); + assertEquals(msg, Bubble.extractFlyoutMessage(mBubbleEntry).message); + } + + @Test + public void testGetUpdateMessage_bigText() { + final String msg = "A big hello there!"; + doReturn(Notification.BigTextStyle.class).when(mNotif).getNotificationStyle(); + mExtras.putCharSequence(Notification.EXTRA_TEXT, "A small hello there."); + mExtras.putCharSequence(Notification.EXTRA_BIG_TEXT, msg); + + // Should be big text, not the small text. + assertEquals(msg, Bubble.extractFlyoutMessage(mBubbleEntry).message); + } + + @Test + public void testGetUpdateMessage_media() { + doReturn(Notification.MediaStyle.class).when(mNotif).getNotificationStyle(); + + // Media notifs don't get update messages. + assertNull(Bubble.extractFlyoutMessage(mBubbleEntry).message); + } + + @Test + public void testGetUpdateMessage_inboxStyle() { + doReturn(Notification.InboxStyle.class).when(mNotif).getNotificationStyle(); + mExtras.putCharSequenceArray( + Notification.EXTRA_TEXT_LINES, + new CharSequence[]{ + "How do you feel about tests?", + "They're okay, I guess.", + "I hate when they're flaky.", + "Really? I prefer them that way."}); + + // Should be the last one only. + assertEquals("Really? I prefer them that way.", + Bubble.extractFlyoutMessage(mBubbleEntry).message); + } + + @Test + public void testGetUpdateMessage_messagingStyle() { + doReturn(Notification.MessagingStyle.class).when(mNotif).getNotificationStyle(); + mExtras.putParcelableArray( + Notification.EXTRA_MESSAGES, + new Bundle[]{ + new Notification.MessagingStyle.Message( + "Hello", 0, "Josh").toBundle(), + new Notification.MessagingStyle.Message( + "Oh, hello!", 0, "Mady").toBundle()}); + + // Should be the last one only. + assertEquals("Oh, hello!", Bubble.extractFlyoutMessage(mBubbleEntry).message); + assertEquals("Mady", Bubble.extractFlyoutMessage(mBubbleEntry).senderName); + } + + @Test + public void testSuppressionListener_change_notified() { + assertThat(mBubble.showInShade()).isTrue(); + + mBubble.setSuppressNotification(true); + + assertThat(mBubble.showInShade()).isFalse(); + + verify(mSuppressionListener).onBubbleNotificationSuppressionChange(mBubble); + } + + @Test + public void testSuppressionListener_noChange_doesntNotify() { + assertThat(mBubble.showInShade()).isTrue(); + + mBubble.setSuppressNotification(false); + + verify(mSuppressionListener, never()).onBubbleNotificationSuppressionChange(any()); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTestActivity.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTestActivity.java new file mode 100644 index 000000000000..d5fbe556045a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTestActivity.java @@ -0,0 +1,40 @@ +/* + * 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.bubbles; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +import com.android.wm.shell.R; + +/** + * Referenced by NotificationTestHelper#makeBubbleMetadata + */ +public class BubblesTestActivity extends Activity { + + public static final String BUBBLE_ACTIVITY_OPENED = "BUBBLE_ACTIVITY_OPENED"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + + Intent i = new Intent(BUBBLE_ACTIVITY_OPENED); + sendBroadcast(i); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/TestableBubblePositioner.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/TestableBubblePositioner.java new file mode 100644 index 000000000000..96bc5335a32c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/TestableBubblePositioner.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.bubbles; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Insets; +import android.graphics.Rect; +import android.view.WindowManager; + +public class TestableBubblePositioner extends BubblePositioner { + + public TestableBubblePositioner(Context context, + WindowManager windowManager) { + super(context, windowManager); + + updateInternal(Configuration.ORIENTATION_PORTRAIT, + Insets.of(0, 0, 0, 0), + new Rect(0, 0, 500, 1000)); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java new file mode 100644 index 000000000000..1eba3c266358 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java @@ -0,0 +1,154 @@ +/* + * 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.bubbles.animation; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.annotation.SuppressLint; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Insets; +import android.graphics.PointF; +import android.graphics.Rect; +import android.testing.AndroidTestingRunner; +import android.view.View; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.R; +import com.android.wm.shell.bubbles.BubblePositioner; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Spy; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestCase { + + private int mDisplayWidth = 500; + private int mDisplayHeight = 1000; + private int mExpandedViewPadding = 10; + + private Runnable mOnBubbleAnimatedOutAction = mock(Runnable.class); + @Spy + ExpandedAnimationController mExpandedController; + + private int mStackOffset; + private PointF mExpansionPoint; + + @SuppressLint("VisibleForTests") + @Before + public void setUp() throws Exception { + super.setUp(); + + BubblePositioner positioner = new BubblePositioner(getContext(), mock(WindowManager.class)); + positioner.updateInternal(Configuration.ORIENTATION_PORTRAIT, + Insets.of(0, 0, 0, 0), + new Rect(0, 0, mDisplayWidth, mDisplayHeight)); + mExpandedController = new ExpandedAnimationController(positioner, mExpandedViewPadding, + mOnBubbleAnimatedOutAction); + + addOneMoreThanBubbleLimitBubbles(); + mLayout.setActiveController(mExpandedController); + + Resources res = mLayout.getResources(); + mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); + mExpansionPoint = new PointF(100, 100); + } + + @Test + @Ignore + public void testExpansionAndCollapse() throws InterruptedException { + Runnable afterExpand = mock(Runnable.class); + mExpandedController.expandFromStack(afterExpand); + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + + testBubblesInCorrectExpandedPositions(); + verify(afterExpand).run(); + + Runnable afterCollapse = mock(Runnable.class); + mExpandedController.collapseBackToStack(mExpansionPoint, afterCollapse); + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + + testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y, -1); + verify(afterExpand).run(); + } + + @Test + @Ignore + public void testOnChildAdded() throws InterruptedException { + expand(); + + // Add another new view and wait for its animation. + final View newView = new FrameLayout(getContext()); + mLayout.addView(newView, 0); + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + + testBubblesInCorrectExpandedPositions(); + } + + @Test + @Ignore + public void testOnChildRemoved() throws InterruptedException { + expand(); + + // Remove some views and see if the remaining child views still pass the expansion test. + mLayout.removeView(mViews.get(0)); + mLayout.removeView(mViews.get(3)); + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + testBubblesInCorrectExpandedPositions(); + } + + /** Expand the stack and wait for animations to finish. */ + private void expand() throws InterruptedException { + mExpandedController.expandFromStack(mock(Runnable.class)); + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + } + + /** Check that children are in the correct positions for being stacked. */ + private void testStackedAtPosition(float x, float y, int offsetMultiplier) { + // Make sure the rest of the stack moved again, including the first bubble not moving, and + // is stacked to the right now that we're on the right side of the screen. + for (int i = 0; i < mLayout.getChildCount(); i++) { + assertEquals(x + i * offsetMultiplier * mStackOffset, + mLayout.getChildAt(i).getTranslationX(), 2f); + assertEquals(y, mLayout.getChildAt(i).getTranslationY(), 2f); + assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f); + } + } + + /** Check that children are in the correct positions for being expanded. */ + private void testBubblesInCorrectExpandedPositions() { + // Check all the visible bubbles to see if they're in the right place. + for (int i = 0; i < mLayout.getChildCount(); i++) { + float expectedPosition = mExpandedController.getBubbleXOrYForOrientation(i); + assertEquals(expectedPosition, + mLayout.getChildAt(i).getTranslationX(), + 2f); + assertEquals(expectedPosition, + mLayout.getChildAt(i).getTranslationY(), 2f); + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTest.java new file mode 100644 index 000000000000..c4edbb286e16 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTest.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.bubbles.animation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.os.SystemClock; +import android.testing.AndroidTestingRunner; +import android.view.View; +import android.widget.FrameLayout; + +import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.dynamicanimation.animation.SpringForce; +import androidx.test.filters.SmallTest; + +import com.google.android.collect.Sets; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.mockito.Mockito; +import org.mockito.Spy; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +/** Tests the PhysicsAnimationLayout itself, with a basic test animation controller. */ +public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { + static final float TEST_TRANSLATION_X_OFFSET = 15f; + + @Spy + private TestableAnimationController mTestableController = new TestableAnimationController(); + + @Before + public void setUp() throws Exception { + super.setUp(); + + // By default, use translation animations, chain the X animations with the default + // offset, and don't actually remove views immediately (since most implementations will wait + // to animate child views out before actually removing them). + mTestableController.setAnimatedProperties(Sets.newHashSet( + DynamicAnimation.TRANSLATION_X, + DynamicAnimation.TRANSLATION_Y)); + mTestableController.setChainedProperties(Sets.newHashSet(DynamicAnimation.TRANSLATION_X)); + mTestableController.setOffsetForProperty( + DynamicAnimation.TRANSLATION_X, TEST_TRANSLATION_X_OFFSET); + mTestableController.setRemoveImmediately(false); + } + + @Test + @Ignore + public void testHierarchyChanges() throws InterruptedException { + mLayout.setActiveController(mTestableController); + addOneMoreThanBubbleLimitBubbles(); + + // Make sure the controller was notified of all the views we added. + for (View mView : mViews) { + Mockito.verify(mTestableController).onChildAdded(mView, 0); + } + + // Remove some views and ensure the controller was notified, with the proper indices. + mTestableController.setRemoveImmediately(true); + mLayout.removeView(mViews.get(1)); + mLayout.removeView(mViews.get(2)); + Mockito.verify(mTestableController).onChildRemoved( + eq(mViews.get(1)), eq(1), any()); + Mockito.verify(mTestableController).onChildRemoved( + eq(mViews.get(2)), eq(1), any()); + + // Make sure we still get view added notifications after doing some removals. + final View newBubble = new FrameLayout(mContext); + mLayout.addView(newBubble, 0); + Mockito.verify(mTestableController).onChildAdded(newBubble, 0); + } + + @Test + @Ignore + public void testUpdateValueNotChained() throws InterruptedException { + mLayout.setActiveController(mTestableController); + addOneMoreThanBubbleLimitBubbles(); + + // Don't chain any values. + mTestableController.setChainedProperties(Sets.newHashSet()); + + // Child views should not be translated. + assertEquals(0, mLayout.getChildAt(0).getTranslationX(), .1f); + assertEquals(0, mLayout.getChildAt(1).getTranslationX(), .1f); + + // Animate the first child's translation X. + final CountDownLatch animLatch = new CountDownLatch(1); + + mTestableController + .animationForChildAtIndex(0) + .translationX(100) + .start(animLatch::countDown); + animLatch.await(1, TimeUnit.SECONDS); + + // Ensure that the first view has been translated, but not the second one. + assertEquals(100, mLayout.getChildAt(0).getTranslationX(), .1f); + assertEquals(0, mLayout.getChildAt(1).getTranslationX(), .1f); + } + + @Test + @Ignore + public void testUpdateValueXChained() throws InterruptedException { + testChainedTranslationAnimations(); + } + + @Test + @Ignore + public void testSetEndActions() throws InterruptedException { + mLayout.setActiveController(mTestableController); + addOneMoreThanBubbleLimitBubbles(); + mTestableController.setChainedProperties(Sets.newHashSet()); + + final CountDownLatch xLatch = new CountDownLatch(1); + Runnable xEndAction = Mockito.spy(new Runnable() { + + @Override + public void run() { + xLatch.countDown(); + } + }); + + final CountDownLatch yLatch = new CountDownLatch(1); + Runnable yEndAction = Mockito.spy(new Runnable() { + + @Override + public void run() { + yLatch.countDown(); + } + }); + + // Set end listeners for both x and y. + mTestableController.setEndActionForProperty(xEndAction, DynamicAnimation.TRANSLATION_X); + mTestableController.setEndActionForProperty(yEndAction, DynamicAnimation.TRANSLATION_Y); + + // Animate x, and wait for it to finish. + mTestableController.animationForChildAtIndex(0) + .translationX(100) + .start(); + + xLatch.await(); + yLatch.await(1, TimeUnit.SECONDS); + + // Make sure the x end listener was called only one time, and the y listener was never + // called since we didn't animate y. Wait 1 second after the original animation end trigger + // to make sure it doesn't get called again. + Mockito.verify(xEndAction, Mockito.after(1000).times(1)).run(); + Mockito.verify(yEndAction, Mockito.after(1000).never()).run(); + } + + @Test + @Ignore + public void testRemoveEndListeners() throws InterruptedException { + mLayout.setActiveController(mTestableController); + addOneMoreThanBubbleLimitBubbles(); + mTestableController.setChainedProperties(Sets.newHashSet()); + + final CountDownLatch xLatch = new CountDownLatch(1); + Runnable xEndListener = Mockito.spy(new Runnable() { + + @Override + public void run() { + xLatch.countDown(); + } + }); + + // Set the end listener. + mTestableController.setEndActionForProperty(xEndListener, DynamicAnimation.TRANSLATION_X); + + // Animate x, and wait for it to finish. + mTestableController.animationForChildAtIndex(0) + .translationX(100) + .start(); + xLatch.await(); + + InOrder endListenerCalls = inOrder(xEndListener); + endListenerCalls.verify(xEndListener, Mockito.times(1)).run(); + + // Animate X again, remove the end listener. + mTestableController.animationForChildAtIndex(0) + .translationX(1000) + .start(); + mTestableController.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X); + xLatch.await(1, TimeUnit.SECONDS); + + // Make sure the end listener was not called. + endListenerCalls.verifyNoMoreInteractions(); + } + + @Test + @Ignore + public void testSetController() throws InterruptedException { + // Add the bubbles, then set the controller, to make sure that a controller added to an + // already-initialized view works correctly. + addOneMoreThanBubbleLimitBubbles(); + mLayout.setActiveController(mTestableController); + testChainedTranslationAnimations(); + + TestableAnimationController secondController = + Mockito.spy(new TestableAnimationController()); + secondController.setAnimatedProperties(Sets.newHashSet( + DynamicAnimation.SCALE_X, DynamicAnimation.SCALE_Y)); + secondController.setChainedProperties(Sets.newHashSet( + DynamicAnimation.SCALE_X)); + secondController.setOffsetForProperty( + DynamicAnimation.SCALE_X, 10f); + secondController.setRemoveImmediately(true); + + mLayout.setActiveController(secondController); + mTestableController.animationForChildAtIndex(0) + .scaleX(1.5f) + .start(); + + waitForPropertyAnimations(DynamicAnimation.SCALE_X); + + // Make sure we never asked the original controller about any SCALE animations, that would + // mean the controller wasn't switched over properly. + Mockito.verify(mTestableController, Mockito.never()) + .getNextAnimationInChain(eq(DynamicAnimation.SCALE_X), anyInt()); + Mockito.verify(mTestableController, Mockito.never()) + .getOffsetForChainedPropertyAnimation(eq(DynamicAnimation.SCALE_X)); + + // Make sure we asked the new controller about its animated properties, and configuration + // options. + Mockito.verify(secondController, Mockito.atLeastOnce()) + .getAnimatedProperties(); + Mockito.verify(secondController, Mockito.atLeastOnce()) + .getNextAnimationInChain(eq(DynamicAnimation.SCALE_X), anyInt()); + Mockito.verify(secondController, Mockito.atLeastOnce()) + .getOffsetForChainedPropertyAnimation(eq(DynamicAnimation.SCALE_X)); + + mLayout.setActiveController(mTestableController); + mTestableController.animationForChildAtIndex(0) + .translationX(100f) + .start(); + + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X); + + // Make sure we never asked the second controller about the TRANSLATION_X animation. + Mockito.verify(secondController, Mockito.never()) + .getNextAnimationInChain(eq(DynamicAnimation.TRANSLATION_X), anyInt()); + Mockito.verify(secondController, Mockito.never()) + .getOffsetForChainedPropertyAnimation(eq(DynamicAnimation.TRANSLATION_X)); + + } + + @Test + @Ignore + public void testArePropertiesAnimating() throws InterruptedException { + mLayout.setActiveController(mTestableController); + addOneMoreThanBubbleLimitBubbles(); + + assertFalse(mLayout.arePropertiesAnimating( + DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)); + + mTestableController.animationForChildAtIndex(0) + .translationX(100f) + .start(); + + // Wait for the animations to get underway. + SystemClock.sleep(50); + + assertTrue(mLayout.arePropertiesAnimating(DynamicAnimation.TRANSLATION_X)); + assertFalse(mLayout.arePropertiesAnimating(DynamicAnimation.TRANSLATION_Y)); + + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X); + + assertFalse(mLayout.arePropertiesAnimating( + DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)); + } + + @Test + @Ignore + public void testCancelAllAnimations() throws InterruptedException { + mLayout.setActiveController(mTestableController); + addOneMoreThanBubbleLimitBubbles(); + + mTestableController.animationForChildAtIndex(0) + .position(1000, 1000) + .start(); + + mLayout.cancelAllAnimations(); + + // Animations should be somewhere before their end point. + assertTrue(mViews.get(0).getTranslationX() < 1000); + assertTrue(mViews.get(0).getTranslationY() < 1000); + } + + /** Standard test of chained translation animations. */ + private void testChainedTranslationAnimations() throws InterruptedException { + mLayout.setActiveController(mTestableController); + addOneMoreThanBubbleLimitBubbles(); + + assertEquals(0, mLayout.getChildAt(0).getTranslationX(), .1f); + assertEquals(0, mLayout.getChildAt(1).getTranslationX(), .1f); + + mTestableController.animationForChildAtIndex(0) + .translationX(100f) + .start(); + + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X); + + for (int i = 0; i < mLayout.getChildCount(); i++) { + assertEquals( + 100 + i * TEST_TRANSLATION_X_OFFSET, + mLayout.getChildAt(i).getTranslationX(), .1f); + } + + // Ensure that the Y translations were unaffected. + assertEquals(0, mLayout.getChildAt(0).getTranslationY(), .1f); + assertEquals(0, mLayout.getChildAt(1).getTranslationY(), .1f); + + // Animate the first child's Y translation. + mTestableController.animationForChildAtIndex(0) + .translationY(100f) + .start(); + + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_Y); + + // Ensure that only the first view's Y translation chained, since we only chained X + // translations. + assertEquals(100, mLayout.getChildAt(0).getTranslationY(), .1f); + assertEquals(0, mLayout.getChildAt(1).getTranslationY(), .1f); + } + + @Test + @Ignore + public void testPhysicsAnimator() throws InterruptedException { + mLayout.setActiveController(mTestableController); + addOneMoreThanBubbleLimitBubbles(); + + Runnable afterAll = Mockito.mock(Runnable.class); + Runnable after = Mockito.spy(new Runnable() { + int mCallCount = 0; + + @Override + public void run() { + // Make sure that if only one of the animations has finished, we didn't already call + // afterAll. + if (mCallCount == 1) { + Mockito.verifyNoMoreInteractions(afterAll); + } + } + }); + + // Animate from x = 7 to x = 100, and from y = 100 to 7 = 200, calling 'after' after each + // property's animation completes, then call afterAll when they're all complete. + mTestableController.animationForChildAtIndex(0) + .translationX(7, 100, after) + .translationY(100, 200, after) + .start(afterAll); + + // We should have immediately set the 'from' values. + assertEquals(7, mViews.get(0).getTranslationX(), .01f); + assertEquals(100, mViews.get(0).getTranslationY(), .01f); + + waitForPropertyAnimations( + DynamicAnimation.TRANSLATION_X, + DynamicAnimation.TRANSLATION_Y); + + // We should have called the after callback twice, and afterAll once. We verify in the + // mocked callback that afterAll isn't called before both finish. + Mockito.verify(after, times(2)).run(); + Mockito.verify(afterAll).run(); + + // Make sure we actually animated the views. + assertEquals(100, mViews.get(0).getTranslationX(), .01f); + assertEquals(200, mViews.get(0).getTranslationY(), .01f); + } + + @Test + @Ignore + public void testAnimationsForChildrenFromIndex() throws InterruptedException { + // Don't chain since we're going to invoke each animation independently. + mTestableController.setChainedProperties(new HashSet<>()); + + mLayout.setActiveController(mTestableController); + + addOneMoreThanBubbleLimitBubbles(); + + Runnable allEnd = Mockito.mock(Runnable.class); + + mTestableController.animationsForChildrenFromIndex( + 1, (index, animation) -> animation.translationX((index - 1) * 50)) + .startAll(allEnd); + + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X); + + assertEquals(0, mViews.get(0).getTranslationX(), .1f); + assertEquals(0, mViews.get(1).getTranslationX(), .1f); + assertEquals(50, mViews.get(2).getTranslationX(), .1f); + assertEquals(100, mViews.get(3).getTranslationX(), .1f); + + Mockito.verify(allEnd, times(1)).run(); + } + + @Test + @Ignore + public void testAnimationsForChildrenFromIndex_noChildren() { + mLayout.setActiveController(mTestableController); + + final Runnable after = Mockito.mock(Runnable.class); + mTestableController + .animationsForChildrenFromIndex(0, (index, animation) -> { }) + .startAll(after); + + verify(after, Mockito.times(1)).run(); + } + + /** + * Animation controller with configuration methods whose return values can be set by individual + * tests. + */ + private class TestableAnimationController + extends PhysicsAnimationLayout.PhysicsAnimationController { + private Set<DynamicAnimation.ViewProperty> mAnimatedProperties = new HashSet<>(); + private Set<DynamicAnimation.ViewProperty> mChainedProperties = new HashSet<>(); + private HashMap<DynamicAnimation.ViewProperty, Float> mOffsetForProperty = new HashMap<>(); + private boolean mRemoveImmediately = false; + + void setAnimatedProperties( + Set<DynamicAnimation.ViewProperty> animatedProperties) { + mAnimatedProperties = animatedProperties; + } + + void setChainedProperties( + Set<DynamicAnimation.ViewProperty> chainedProperties) { + mChainedProperties = chainedProperties; + } + + void setOffsetForProperty( + DynamicAnimation.ViewProperty property, float offset) { + mOffsetForProperty.put(property, offset); + } + + public void setRemoveImmediately(boolean removeImmediately) { + mRemoveImmediately = removeImmediately; + } + + @Override + Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { + return mAnimatedProperties; + } + + @Override + int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) { + return mChainedProperties.contains(property) ? index + 1 : NONE; + } + + @Override + float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) { + return mOffsetForProperty.getOrDefault(property, 0f); + } + + @Override + SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) { + return new SpringForce(); + } + + @Override + void onChildAdded(View child, int index) {} + + @Override + void onChildRemoved(View child, int index, Runnable finishRemoval) { + if (mRemoveImmediately) { + finishRemoval.run(); + } + } + + @Override + void onChildReordered(View child, int oldIndex, int newIndex) {} + + @Override + void onActiveControllerForLayout(PhysicsAnimationLayout layout) {} + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java new file mode 100644 index 000000000000..a7a7db869776 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java @@ -0,0 +1,324 @@ +/* + * 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.bubbles.animation; + +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.view.DisplayCutout; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowInsets; +import android.widget.FrameLayout; + +import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.dynamicanimation.animation.SpringForce; + +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTestCase; + +import org.junit.Before; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Test case for tests that involve the {@link PhysicsAnimationLayout}. This test case constructs a + * testable version of the layout, and provides some helpful methods to add views to the layout and + * wait for physics animations to finish running. + * + * See physics-animation-testing.md. + */ +public class PhysicsAnimationLayoutTestCase extends ShellTestCase { + TestablePhysicsAnimationLayout mLayout; + List<View> mViews = new ArrayList<>(); + + Handler mMainThreadHandler; + + int mSystemWindowInsetSize = 50; + int mCutoutInsetSize = 100; + + int mWidth = 1000; + int mHeight = 1000; + + @Mock + private WindowInsets mWindowInsets; + + @Mock + private DisplayCutout mCutout; + + protected int mMaxBubbles; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mLayout = new TestablePhysicsAnimationLayout(mContext); + mLayout.setLeft(0); + mLayout.setRight(mWidth); + mLayout.setTop(0); + mLayout.setBottom(mHeight); + + mMaxBubbles = + getContext().getResources().getInteger(R.integer.bubbles_max_rendered); + mMainThreadHandler = new Handler(Looper.getMainLooper()); + + when(mWindowInsets.getSystemWindowInsetTop()).thenReturn(mSystemWindowInsetSize); + when(mWindowInsets.getSystemWindowInsetBottom()).thenReturn(mSystemWindowInsetSize); + when(mWindowInsets.getSystemWindowInsetLeft()).thenReturn(mSystemWindowInsetSize); + when(mWindowInsets.getSystemWindowInsetRight()).thenReturn(mSystemWindowInsetSize); + + when(mWindowInsets.getDisplayCutout()).thenReturn(mCutout); + when(mCutout.getSafeInsetTop()).thenReturn(mCutoutInsetSize); + when(mCutout.getSafeInsetBottom()).thenReturn(mCutoutInsetSize); + when(mCutout.getSafeInsetLeft()).thenReturn(mCutoutInsetSize); + when(mCutout.getSafeInsetRight()).thenReturn(mCutoutInsetSize); + } + + /** Add one extra bubble over the limit, so we can make sure it's gone/chains appropriately. */ + void addOneMoreThanBubbleLimitBubbles() throws InterruptedException { + for (int i = 0; i < mMaxBubbles + 1; i++) { + final View newView = new FrameLayout(mContext); + mLayout.addView(newView, 0); + mViews.add(0, newView); + + newView.setTranslationX(0); + newView.setTranslationY(0); + } + } + + /** + * Uses a {@link java.util.concurrent.CountDownLatch} to wait for the given properties' + * animations to finish before allowing the test to proceed. + */ + void waitForPropertyAnimations(DynamicAnimation.ViewProperty... properties) + throws InterruptedException { + final CountDownLatch animLatch = new CountDownLatch(properties.length); + for (DynamicAnimation.ViewProperty property : properties) { + mLayout.setTestEndActionForProperty(animLatch::countDown, property); + } + + animLatch.await(2, TimeUnit.SECONDS); + } + + /** Uses a latch to wait for the main thread message queue to finish. */ + void waitForLayoutMessageQueue() throws InterruptedException { + CountDownLatch layoutLatch = new CountDownLatch(1); + mMainThreadHandler.post(layoutLatch::countDown); + layoutLatch.await(2, TimeUnit.SECONDS); + } + + /** + * Testable subclass of the PhysicsAnimationLayout that ensures methods that trigger animations + * are run on the main thread, which is a requirement of DynamicAnimation. + */ + protected class TestablePhysicsAnimationLayout extends PhysicsAnimationLayout { + public TestablePhysicsAnimationLayout(Context context) { + super(context); + } + + @Override + protected boolean isActiveController(PhysicsAnimationController controller) { + // Return true since otherwise all test controllers will be seen as inactive since they + // are wrapped by MainThreadAnimationControllerWrapper. + return true; + } + + @Override + public boolean post(Runnable action) { + return mMainThreadHandler.post(action); + } + + @Override + public boolean postDelayed(Runnable action, long delayMillis) { + return mMainThreadHandler.postDelayed(action, delayMillis); + } + + @Override + public void setActiveController(PhysicsAnimationController controller) { + runOnMainThreadAndBlock( + () -> super.setActiveController( + new MainThreadAnimationControllerWrapper(controller))); + } + + @Override + public void cancelAllAnimations() { + mMainThreadHandler.post(super::cancelAllAnimations); + } + + @Override + public void cancelAnimationsOnView(View view) { + mMainThreadHandler.post(() -> super.cancelAnimationsOnView(view)); + } + + @Override + public WindowInsets getRootWindowInsets() { + return mWindowInsets; + } + + @Override + public void addView(View child, int index) { + child.setTag(R.id.physics_animator_tag, new TestablePhysicsPropertyAnimator(child)); + super.addView(child, index); + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + child.setTag(R.id.physics_animator_tag, new TestablePhysicsPropertyAnimator(child)); + super.addView(child, index, params); + } + + /** + * Sets an end action that will be called after the 'real' end action that was already set. + */ + private void setTestEndActionForProperty( + Runnable action, DynamicAnimation.ViewProperty property) { + final Runnable realEndAction = mEndActionForProperty.get(property); + mLayout.mEndActionForProperty.put(property, () -> { + if (realEndAction != null) { + realEndAction.run(); + } + + action.run(); + }); + } + + /** PhysicsPropertyAnimator that posts its animations to the main thread. */ + protected class TestablePhysicsPropertyAnimator extends PhysicsPropertyAnimator { + public TestablePhysicsPropertyAnimator(View view) { + super(view); + } + + @Override + protected void animateValueForChild(DynamicAnimation.ViewProperty property, View view, + float value, float startVel, long startDelay, float stiffness, + float dampingRatio, Runnable[] afterCallbacks) { + mMainThreadHandler.post(() -> super.animateValueForChild( + property, view, value, startVel, startDelay, stiffness, dampingRatio, + afterCallbacks)); + } + + @Override + protected void startPathAnimation() { + mMainThreadHandler.post(super::startPathAnimation); + } + } + + /** + * Wrapper around an animation controller that dispatches methods that could start + * animations to the main thread. + */ + protected class MainThreadAnimationControllerWrapper extends PhysicsAnimationController { + + private final PhysicsAnimationController mWrappedController; + + protected MainThreadAnimationControllerWrapper(PhysicsAnimationController controller) { + mWrappedController = controller; + } + + @Override + protected void setLayout(PhysicsAnimationLayout layout) { + mWrappedController.setLayout(layout); + } + + @Override + protected PhysicsAnimationLayout getLayout() { + return mWrappedController.getLayout(); + } + + @Override + Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { + return mWrappedController.getAnimatedProperties(); + } + + @Override + int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) { + return mWrappedController.getNextAnimationInChain(property, index); + } + + @Override + float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) { + return mWrappedController.getOffsetForChainedPropertyAnimation(property); + } + + @Override + SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) { + return mWrappedController.getSpringForce(property, view); + } + + @Override + void onChildAdded(View child, int index) { + runOnMainThreadAndBlock(() -> mWrappedController.onChildAdded(child, index)); + } + + @Override + void onChildRemoved(View child, int index, Runnable finishRemoval) { + runOnMainThreadAndBlock( + () -> mWrappedController.onChildRemoved(child, index, finishRemoval)); + } + + @Override + void onChildReordered(View child, int oldIndex, int newIndex) { + runOnMainThreadAndBlock( + () -> mWrappedController.onChildReordered(child, oldIndex, newIndex)); + } + + @Override + void onActiveControllerForLayout(PhysicsAnimationLayout layout) { + runOnMainThreadAndBlock( + () -> mWrappedController.onActiveControllerForLayout(layout)); + } + + @Override + protected PhysicsPropertyAnimator animationForChild(View child) { + PhysicsPropertyAnimator animator = + (PhysicsPropertyAnimator) child.getTag(R.id.physics_animator_tag); + + if (!(animator instanceof TestablePhysicsPropertyAnimator)) { + animator = new TestablePhysicsPropertyAnimator(child); + child.setTag(R.id.physics_animator_tag, animator); + } + + return animator; + } + } + } + + /** + * Posts the given Runnable on the main thread, and blocks the calling thread until it's run. + */ + private void runOnMainThreadAndBlock(Runnable action) { + final CountDownLatch latch = new CountDownLatch(1); + mMainThreadHandler.post(() -> { + action.run(); + latch.countDown(); + }); + + try { + latch.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/StackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/StackAnimationControllerTest.java new file mode 100644 index 000000000000..f36dcbe7bf4b --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/StackAnimationControllerTest.java @@ -0,0 +1,334 @@ +/* + * 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.bubbles.animation; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.graphics.PointF; +import android.testing.AndroidTestingRunner; +import android.view.View; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.dynamicanimation.animation.SpringForce; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.R; +import com.android.wm.shell.bubbles.TestableBubblePositioner; +import com.android.wm.shell.common.FloatingContentCoordinator; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.IntSupplier; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class StackAnimationControllerTest extends PhysicsAnimationLayoutTestCase { + + @Mock + private FloatingContentCoordinator mFloatingContentCoordinator; + + private TestableStackController mStackController; + + private int mStackOffset; + private Runnable mCheckStartPosSet; + + @Before + public void setUp() throws Exception { + super.setUp(); + mStackController = spy(new TestableStackController( + mFloatingContentCoordinator, new IntSupplier() { + @Override + public int getAsInt() { + return mLayout.getChildCount(); + } + }, mock(Runnable.class))); + mLayout.setActiveController(mStackController); + addOneMoreThanBubbleLimitBubbles(); + mStackOffset = mLayout.getResources().getDimensionPixelSize(R.dimen.bubble_stack_offset); + } + + /** + * Test moving around the stack, and make sure the position is updated correctly, and the stack + * direction is correct. + */ + @Test + @Ignore("Flaking") + public void testMoveFirstBubbleWithStackFollowing() throws InterruptedException { + mStackController.moveFirstBubbleWithStackFollowing(200, 100); + + // The first bubble should have moved instantly, the rest should be waiting for animation. + assertEquals(200, mViews.get(0).getTranslationX(), .1f); + assertEquals(100, mViews.get(0).getTranslationY(), .1f); + assertEquals(0, mViews.get(1).getTranslationX(), .1f); + assertEquals(0, mViews.get(1).getTranslationY(), .1f); + + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + + // Make sure the rest of the stack got moved to the right place and is stacked to the left. + testStackedAtPosition(200, 100, -1); + assertEquals(new PointF(200, 100), mStackController.getStackPosition()); + + mStackController.moveFirstBubbleWithStackFollowing(1000, 500); + + // The first bubble again should have moved instantly while the rest remained where they + // were until the animation takes over. + assertEquals(1000, mViews.get(0).getTranslationX(), .1f); + assertEquals(500, mViews.get(0).getTranslationY(), .1f); + assertEquals(200 + -mStackOffset, mViews.get(1).getTranslationX(), .1f); + assertEquals(100, mViews.get(1).getTranslationY(), .1f); + + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + + // Make sure the rest of the stack moved again, including the first bubble not moving, and + // is stacked to the right now that we're on the right side of the screen. + testStackedAtPosition(1000, 500, 1); + assertEquals(new PointF(1000, 500), mStackController.getStackPosition()); + } + + @Test + @Ignore("Sporadically failing due to DynamicAnimation not settling.") + public void testFlingSideways() throws InterruptedException { + // Hard fling directly upwards, no X velocity. The X fling should terminate pretty much + // immediately, and spring to 0f, the y fling is hard enough that it will overshoot the top + // but should bounce back down. + mStackController.flingThenSpringFirstBubbleWithStackFollowing( + DynamicAnimation.TRANSLATION_X, + 5000f, 1.15f, new SpringForce(), mWidth * 1f); + mStackController.flingThenSpringFirstBubbleWithStackFollowing( + DynamicAnimation.TRANSLATION_Y, + 0f, 1.15f, new SpringForce(), 0f); + + // Nothing should move initially since the animations haven't begun, including the first + // view. + assertEquals(0f, mViews.get(0).getTranslationX(), 1f); + assertEquals(0f, mViews.get(0).getTranslationY(), 1f); + + // Wait for the flinging. + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, + DynamicAnimation.TRANSLATION_Y); + + // Wait for the springing. + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, + DynamicAnimation.TRANSLATION_Y); + + // Once the dust has settled, we should have flung all the way to the right side, with the + // stack stacked off to the right now. + testStackedAtPosition(mWidth * 1f, 0f, 1); + } + + @Test + @Ignore("Sporadically failing due to DynamicAnimation not settling.") + public void testFlingUpFromBelowBottomCenter() throws InterruptedException { + // Move to the center of the screen, just past the bottom. + mStackController.moveFirstBubbleWithStackFollowing(mWidth / 2f, mHeight + 100); + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + + // Hard fling directly upwards, no X velocity. The X fling should terminate pretty much + // immediately, and spring to 0f, the y fling is hard enough that it will overshoot the top + // but should bounce back down. + mStackController.flingThenSpringFirstBubbleWithStackFollowing( + DynamicAnimation.TRANSLATION_X, + 0, 1.15f, new SpringForce(), 27f); + mStackController.flingThenSpringFirstBubbleWithStackFollowing( + DynamicAnimation.TRANSLATION_Y, + 5000f, 1.15f, new SpringForce(), 27f); + + // Nothing should move initially since the animations haven't begun. + assertEquals(mWidth / 2f, mViews.get(0).getTranslationX(), .1f); + assertEquals(mHeight + 100, mViews.get(0).getTranslationY(), .1f); + + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, + DynamicAnimation.TRANSLATION_Y); + + // Once the dust has settled, we should have flung a bit but then sprung to the final + // destination which is (27, 27). + testStackedAtPosition(27, 27, -1); + } + + @Test + @Ignore("Flaking") + public void testChildAdded() throws InterruptedException { + // Move the stack to y = 500. + mStackController.moveFirstBubbleWithStackFollowing(0f, 500f); + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, + DynamicAnimation.TRANSLATION_Y); + + final View newView = new FrameLayout(mContext); + mLayout.addView( + newView, + 0, + new FrameLayout.LayoutParams(50, 50)); + + waitForStartPosToBeSet(); + waitForLayoutMessageQueue(); + waitForPropertyAnimations( + DynamicAnimation.TRANSLATION_X, + DynamicAnimation.TRANSLATION_Y, + DynamicAnimation.SCALE_X, + DynamicAnimation.SCALE_Y); + + // The new view should be at the top of the stack, in the correct position. + assertEquals(0f, newView.getTranslationX(), .1f); + assertEquals(500f, newView.getTranslationY(), .1f); + assertEquals(1f, newView.getScaleX(), .1f); + assertEquals(1f, newView.getScaleY(), .1f); + assertEquals(1f, newView.getAlpha(), .1f); + } + + @Test + @Ignore("Occasionally flakes, ignoring pending investigation.") + public void testChildRemoved() throws InterruptedException { + assertEquals(0, mLayout.getTransientViewCount()); + + final View firstView = mLayout.getChildAt(0); + mLayout.removeView(firstView); + + // The view should now be transient, and missing from the view's normal hierarchy. + assertEquals(1, mLayout.getTransientViewCount()); + assertEquals(-1, mLayout.indexOfChild(firstView)); + + waitForPropertyAnimations(DynamicAnimation.ALPHA); + waitForLayoutMessageQueue(); + + // The view should now be gone entirely, no transient views left. + assertEquals(0, mLayout.getTransientViewCount()); + + // The subsequent view should have been translated over to 0, not stacked off to the left. + assertEquals(0, mLayout.getChildAt(0).getTranslationX(), .1f); + } + + @Test + @Ignore("Flaky") + public void testRestoredAtRestingPosition() throws InterruptedException { + mStackController.flingStackThenSpringToEdge(0, 5000, 5000); + + waitForPropertyAnimations( + DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + waitForLayoutMessageQueue(); + + final PointF prevStackPos = mStackController.getStackPosition(); + + mLayout.removeAllViews(); + + waitForLayoutMessageQueue(); + + mLayout.addView(new FrameLayout(getContext())); + + waitForLayoutMessageQueue(); + waitForPropertyAnimations( + DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + + assertEquals(prevStackPos, mStackController.getStackPosition()); + } + + @Test + public void testFloatingCoordinator() { + // We should have called onContentAdded only once while adding all of the bubbles in + // setup(). + verify(mFloatingContentCoordinator, times(1)).onContentAdded(any()); + verify(mFloatingContentCoordinator, never()).onContentRemoved(any()); + + // Remove all views and verify that we called onContentRemoved only once. + while (mLayout.getChildCount() > 0) { + mLayout.removeView(mLayout.getChildAt(0)); + } + + verify(mFloatingContentCoordinator, times(1)).onContentRemoved(any()); + } + + /** + * Checks every child view to make sure it's stacked at the given coordinates, off to the left + * or right side depending on offset multiplier. + */ + private void testStackedAtPosition(float x, float y, int offsetMultiplier) { + // Make sure the rest of the stack moved again, including the first bubble not moving, and + // is stacked to the right now that we're on the right side of the screen. + for (int i = 0; i < mLayout.getChildCount(); i++) { + assertEquals(x + i * offsetMultiplier * mStackOffset, + mViews.get(i).getTranslationX(), 2f); + assertEquals(y, mViews.get(i).getTranslationY(), 2f); + } + } + + /** Waits up to 2 seconds for the initial stack position to be initialized. */ + private void waitForStartPosToBeSet() throws InterruptedException { + final CountDownLatch animLatch = new CountDownLatch(1); + + mCheckStartPosSet = () -> { + if (mStackController.getStackPosition().x >= 0) { + animLatch.countDown(); + } else { + mMainThreadHandler.post(mCheckStartPosSet); + } + }; + + mMainThreadHandler.post(mCheckStartPosSet); + + try { + animLatch.await(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + mMainThreadHandler.removeCallbacks(mCheckStartPosSet); + throw e; + } + } + + /** + * Testable version of the stack controller that dispatches its animations on the main thread. + */ + private class TestableStackController extends StackAnimationController { + TestableStackController( + FloatingContentCoordinator floatingContentCoordinator, + IntSupplier bubbleCountSupplier, + Runnable onBubbleAnimatedOutAction) { + super(floatingContentCoordinator, + bubbleCountSupplier, + onBubbleAnimatedOutAction, + new TestableBubblePositioner(mContext, mock(WindowManager.class))); + } + + @Override + protected void flingThenSpringFirstBubbleWithStackFollowing( + DynamicAnimation.ViewProperty property, float vel, float friction, + SpringForce spring, Float finalPosition) { + mMainThreadHandler.post(() -> + super.flingThenSpringFirstBubbleWithStackFollowing( + property, vel, friction, spring, finalPosition)); + } + + @Override + protected void springFirstBubbleWithStackFollowing(DynamicAnimation.ViewProperty property, + SpringForce spring, float vel, float finalPosition, Runnable... after) { + mMainThreadHandler.post(() -> + super.springFirstBubbleWithStackFollowing( + property, spring, vel, finalPosition, after)); + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt new file mode 100644 index 000000000000..416028088294 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt @@ -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.bubbles.storage + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertNotNull +import junit.framework.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class BubblePersistentRepositoryTest : ShellTestCase() { + + private val bubbles = listOf( + BubbleEntity(0, "com.example.messenger", "shortcut-1", "key-1", 120, 0), + BubbleEntity(10, "com.example.chat", "alice and bob", "key-2", 0, 16537428, "title"), + BubbleEntity(0, "com.example.messenger", "shortcut-2", "key-3", 120, 0) + ) + private lateinit var repository: BubblePersistentRepository + + @Before + fun setup() { + repository = BubblePersistentRepository(mContext) + } + + @Test + fun testReadWriteOperation() { + // Verify read before write doesn't cause FileNotFoundException + val actual = repository.readFromDisk() + assertNotNull(actual) + assertTrue(actual.isEmpty()) + + repository.persistsToDisk(bubbles) + assertEquals(bubbles, repository.readFromDisk()) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt new file mode 100644 index 000000000000..4fab9a5496ec --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.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.bubbles.storage + +import android.content.pm.LauncherApps +import android.os.UserHandle +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.util.mockito.eq +import com.android.wm.shell.ShellTestCase +import junit.framework.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class BubbleVolatileRepositoryTest : ShellTestCase() { + + private val user0 = UserHandle.of(0) + private val user10 = UserHandle.of(10) + + private val bubble1 = BubbleEntity(0, "com.example.messenger", "shortcut-1", "key-1", 120, 0) + private val bubble2 = BubbleEntity(10, "com.example.chat", "alice and bob", + "key-2", 0, 16537428, "title") + private val bubble3 = BubbleEntity(0, "com.example.messenger", "shortcut-2", "key-3", 120, 0) + + private val bubbles = listOf(bubble1, bubble2, bubble3) + + private lateinit var repository: BubbleVolatileRepository + private lateinit var launcherApps: LauncherApps + + @Before + fun setup() { + launcherApps = mock(LauncherApps::class.java) + repository = BubbleVolatileRepository(launcherApps) + } + + @Test + fun testAddBubbles() { + repository.addBubbles(bubbles) + assertEquals(bubbles, repository.bubbles) + verify(launcherApps).cacheShortcuts(eq(PKG_MESSENGER), + eq(listOf("shortcut-1", "shortcut-2")), eq(user0), + eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS)) + verify(launcherApps).cacheShortcuts(eq(PKG_CHAT), + eq(listOf("alice and bob")), eq(user10), + eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS)) + + repository.addBubbles(listOf(bubble1)) + assertEquals(listOf(bubble2, bubble3, bubble1), repository.bubbles) + verifyNoMoreInteractions(launcherApps) + } + + @Test + fun testRemoveBubbles() { + repository.addBubbles(bubbles) + assertEquals(bubbles, repository.bubbles) + + repository.removeBubbles(listOf(bubble3)) + assertEquals(listOf(bubble1, bubble2), repository.bubbles) + verify(launcherApps).uncacheShortcuts(eq(PKG_MESSENGER), + eq(listOf("shortcut-2")), eq(user0), + eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS)) + } + + @Test + fun testAddAndRemoveBubblesWhenExceedingCapacity() { + repository.capacity = 2 + // push bubbles beyond capacity + repository.addBubbles(bubbles) + // verify it is trim down to capacity + assertEquals(listOf(bubble2, bubble3), repository.bubbles) + verify(launcherApps).cacheShortcuts(eq(PKG_MESSENGER), + eq(listOf("shortcut-2")), eq(user0), + eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS)) + verify(launcherApps).cacheShortcuts(eq(PKG_CHAT), + eq(listOf("alice and bob")), eq(user10), + eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS)) + + repository.addBubbles(listOf(bubble1)) + // verify the oldest bubble is popped + assertEquals(listOf(bubble3, bubble1), repository.bubbles) + verify(launcherApps).uncacheShortcuts(eq(PKG_CHAT), + eq(listOf("alice and bob")), eq(user10), + eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS)) + } + + @Test + fun testAddBubbleMatchesByKey() { + val bubble = BubbleEntity(0, "com.example.pkg", "shortcut-id", "key", 120, 0, "title") + repository.addBubbles(listOf(bubble)) + assertEquals(bubble, repository.bubbles.get(0)) + + // Same key as first bubble but different entry + val bubbleModified = BubbleEntity(0, "com.example.pkg", "shortcut-id", "key", 120, 0, + "different title") + repository.addBubbles(listOf(bubbleModified)) + assertEquals(bubbleModified, repository.bubbles.get(0)) + } +} + +private const val PKG_MESSENGER = "com.example.messenger" +private const val PKG_CHAT = "com.example.chat" diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelperTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelperTest.kt new file mode 100644 index 000000000000..e0891a95c6a6 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelperTest.kt @@ -0,0 +1,82 @@ +/* + * 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.bubbles.storage + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class BubbleXmlHelperTest : ShellTestCase() { + + private val bubbles = listOf( + BubbleEntity(0, "com.example.messenger", "shortcut-1", "k1", 120, 0), + BubbleEntity(10, "com.example.chat", "alice and bob", "k2", 0, 16537428, "title"), + BubbleEntity(0, "com.example.messenger", "shortcut-2", "k3", 120, 0) + ) + + @Test + fun testWriteXml() { + val expectedEntries = """ +<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="k1" h="120" hid="0" /> +<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="k2" h="0" hid="16537428" t="title" /> +<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="k3" h="120" hid="0" /> + """.trimIndent() + ByteArrayOutputStream().use { + writeXml(it, bubbles) + val actual = it.toString() + assertTrue("cannot find expected entry in \n$actual", + actual.contains(expectedEntries)) + } + } + + @Test + fun testReadXml() { + val src = """ +<?xml version='1.0' encoding='utf-8' standalone='yes' ?> +<bs v="1"> +<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="k1" h="120" hid="0" /> +<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="k2" h="0" hid="16537428" t="title" /> +<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="k3" h="120" hid="0" /> +</bs> + """.trimIndent() + val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8))) + assertEquals("failed parsing bubbles from xml\n$src", bubbles, actual) + } + + // TODO: We should handle upgrades gracefully but this is v1 + @Test + fun testUpgradeDropsPreviousData() { + val src = """ +<?xml version='1.0' encoding='utf-8' standalone='yes' ?> +<bs> +<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="k1" h="120" hid="0" /> +<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="k2" h="0" hid="16537428" t="title" /> +<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="k3" h="120" hid="0" /> +</bs> + """.trimIndent() + val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8))) + assertEquals("failed parsing bubbles from xml\n$src", emptyList<BubbleEntity>(), actual) + } +}
\ No newline at end of file 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..4cedc483fc21 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java @@ -0,0 +1,133 @@ +/* + * 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 android.view.WindowInsets.Type.ime; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +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.verifyZeroInteractions; + +import android.graphics.Point; +import android.view.InsetsSource; +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; + +import java.util.concurrent.Executor; + +@SmallTest +public class DisplayImeControllerTest { + + private SurfaceControl.Transaction mT; + private DisplayImeController.PerDisplay mPerDisplay; + private IInputMethodManager mMock; + private Executor mExecutor; + + @Before + public void setUp() throws Exception { + mT = mock(SurfaceControl.Transaction.class); + mMock = mock(IInputMethodManager.class); + mExecutor = spy(Runnable::run); + mPerDisplay = new DisplayImeController(null, null, mExecutor, new TransactionPool() { + @Override + public SurfaceControl.Transaction acquire() { + return mT; + } + + @Override + public void release(SurfaceControl.Transaction t) { + } + }) { + @Override + public IInputMethodManager getImms() { + return mMock; + } + @Override + void removeImeSurface() { } + }.new PerDisplay(DEFAULT_DISPLAY, ROTATION_0); + } + + @Test + public void insetsControlChanged_schedulesNoWorkOnExecutor() { + mPerDisplay.insetsControlChanged(insetsStateWithIme(false), insetsSourceControl()); + verifyZeroInteractions(mExecutor); + } + + @Test + public void insetsChanged_schedulesNoWorkOnExecutor() { + mPerDisplay.insetsChanged(insetsStateWithIme(false)); + verifyZeroInteractions(mExecutor); + } + + @Test + public void showInsets_schedulesNoWorkOnExecutor() { + mPerDisplay.showInsets(ime(), true); + verifyZeroInteractions(mExecutor); + } + + @Test + public void hideInsets_schedulesNoWorkOnExecutor() { + mPerDisplay.hideInsets(ime(), true); + verifyZeroInteractions(mExecutor); + } + + @Test + public void reappliesVisibilityToChangedLeash() { + verifyZeroInteractions(mT); + mPerDisplay.mImeShowing = true; + + mPerDisplay.insetsControlChanged(insetsStateWithIme(false), insetsSourceControl()); + + assertFalse(mPerDisplay.mImeShowing); + verify(mT).hide(any()); + + mPerDisplay.mImeShowing = true; + mPerDisplay.insetsControlChanged(insetsStateWithIme(true), insetsSourceControl()); + + assertTrue(mPerDisplay.mImeShowing); + verify(mT).show(any()); + } + + private InsetsSourceControl[] insetsSourceControl() { + return new InsetsSourceControl[]{ + new InsetsSourceControl(ITYPE_IME, mock(SurfaceControl.class), new Point(0, 0)) + }; + } + + private InsetsState insetsStateWithIme(boolean visible) { + InsetsState state = new InsetsState(); + state.addSource(new InsetsSource(ITYPE_IME)); + state.setSourceVisible(ITYPE_IME, visible); + return state; + } + +} 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/TaskStackListenerImplTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java new file mode 100644 index 000000000000..21bc32c6563c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java @@ -0,0 +1,250 @@ +/* + * 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 org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager; +import android.app.IActivityTaskManager; +import android.content.ComponentName; +import android.os.Handler; +import android.os.Message; +import android.os.RemoteException; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.window.TaskSnapshot; + +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; + +/** + * Tests for {@link com.android.wm.shell.common.TaskStackListenerImpl}. + */ +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +@SmallTest +public class TaskStackListenerImplTest { + + @Mock + private IActivityTaskManager mActivityTaskManager; + + @Mock + private TaskStackListenerCallback mCallback; + + @Mock + private TaskStackListenerCallback mOtherCallback; + + private TaskStackListenerImpl mImpl; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mImpl = new TaskStackListenerImpl(mActivityTaskManager); + mImpl.setHandler(new ProxyToListenerImplHandler(mImpl)); + mImpl.addListener(mCallback); + mImpl.addListener(mOtherCallback); + } + + @Test + public void testAddRemoveMultipleListeners_ExpectRegisterUnregisterOnce() + throws RemoteException { + TaskStackListenerImpl impl = new TaskStackListenerImpl(mActivityTaskManager); + impl.setHandler(new ProxyToListenerImplHandler(impl)); + reset(mActivityTaskManager); + impl.addListener(mCallback); + impl.addListener(mOtherCallback); + verify(mActivityTaskManager, times(1)).registerTaskStackListener(any()); + + impl.removeListener(mOtherCallback); + impl.removeListener(mCallback); + verify(mActivityTaskManager, times(1)).unregisterTaskStackListener(any()); + } + + @Test + public void testOnRecentTaskListUpdated() { + mImpl.onRecentTaskListUpdated(); + verify(mCallback).onRecentTaskListUpdated(); + verify(mOtherCallback).onRecentTaskListUpdated(); + } + + @Test + public void testOnRecentTaskListFrozenChanged() { + mImpl.onRecentTaskListFrozenChanged(true); + verify(mCallback).onRecentTaskListFrozenChanged(eq(true)); + verify(mOtherCallback).onRecentTaskListFrozenChanged(eq(true)); + } + + @Test + public void testOnTaskStackChanged() { + mImpl.onTaskStackChanged(); + verify(mCallback).onTaskStackChangedBackground(); + verify(mCallback).onTaskStackChanged(); + verify(mOtherCallback).onTaskStackChangedBackground(); + verify(mOtherCallback).onTaskStackChanged(); + } + + @Test + public void testOnTaskProfileLocked() { + mImpl.onTaskProfileLocked(1, 2); + verify(mCallback).onTaskProfileLocked(eq(1), eq(2)); + verify(mOtherCallback).onTaskProfileLocked(eq(1), eq(2)); + } + + @Test + public void testOnTaskDisplayChanged() { + mImpl.onTaskDisplayChanged(1, 2); + verify(mCallback).onTaskDisplayChanged(eq(1), eq(2)); + verify(mOtherCallback).onTaskDisplayChanged(eq(1), eq(2)); + } + + @Test + public void testOnTaskCreated() { + mImpl.onTaskCreated(1, new ComponentName("a", "b")); + verify(mCallback).onTaskCreated(eq(1), eq(new ComponentName("a", "b"))); + verify(mOtherCallback).onTaskCreated(eq(1), eq(new ComponentName("a", "b"))); + } + + @Test + public void testOnTaskRemoved() { + mImpl.onTaskRemoved(123); + verify(mCallback).onTaskRemoved(eq(123)); + verify(mOtherCallback).onTaskRemoved(eq(123)); + } + + @Test + public void testOnTaskMovedToFront() { + ActivityManager.RunningTaskInfo info = mock(ActivityManager.RunningTaskInfo.class); + mImpl.onTaskMovedToFront(info); + verify(mCallback).onTaskMovedToFront(eq(info)); + verify(mOtherCallback).onTaskMovedToFront(eq(info)); + } + + @Test + public void testOnTaskDescriptionChanged() { + ActivityManager.RunningTaskInfo info = mock(ActivityManager.RunningTaskInfo.class); + mImpl.onTaskDescriptionChanged(info); + verify(mCallback).onTaskDescriptionChanged(eq(info)); + verify(mOtherCallback).onTaskDescriptionChanged(eq(info)); + } + + @Test + public void testOnTaskSnapshotChanged() { + TaskSnapshot snapshot = mock(TaskSnapshot.class); + mImpl.onTaskSnapshotChanged(123, snapshot); + verify(mCallback).onTaskSnapshotChanged(eq(123), eq(snapshot)); + verify(mOtherCallback).onTaskSnapshotChanged(eq(123), eq(snapshot)); + } + + @Test + public void testOnBackPressedOnTaskRoot() { + ActivityManager.RunningTaskInfo info = mock(ActivityManager.RunningTaskInfo.class); + mImpl.onBackPressedOnTaskRoot(info); + verify(mCallback).onBackPressedOnTaskRoot(eq(info)); + verify(mOtherCallback).onBackPressedOnTaskRoot(eq(info)); + } + + @Test + public void testOnActivityRestartAttempt() { + ActivityManager.RunningTaskInfo info = mock(ActivityManager.RunningTaskInfo.class); + mImpl.onActivityRestartAttempt(info, true, true, true); + verify(mCallback).onActivityRestartAttempt(eq(info), eq(true), eq(true), eq(true)); + verify(mOtherCallback).onActivityRestartAttempt(eq(info), eq(true), eq(true), eq(true)); + } + + @Test + public void testOnActivityPinned() { + mImpl.onActivityPinned("abc", 1, 2, 3); + verify(mCallback).onActivityPinned(eq("abc"), eq(1), eq(2), eq(3)); + verify(mOtherCallback).onActivityPinned(eq("abc"), eq(1), eq(2), eq(3)); + } + + @Test + public void testOnActivityUnpinned() { + mImpl.onActivityUnpinned(); + verify(mCallback).onActivityUnpinned(); + verify(mOtherCallback).onActivityUnpinned(); + } + + @Test + public void testOnActivityForcedResizable() { + mImpl.onActivityForcedResizable("abc", 1, 2); + verify(mCallback).onActivityForcedResizable(eq("abc"), eq(1), eq(2)); + verify(mOtherCallback).onActivityForcedResizable(eq("abc"), eq(1), eq(2)); + } + + @Test + public void testOnActivityDismissingDockedStack() { + mImpl.onActivityDismissingDockedStack(); + verify(mCallback).onActivityDismissingDockedStack(); + verify(mOtherCallback).onActivityDismissingDockedStack(); + } + + @Test + public void testOnActivityLaunchOnSecondaryDisplayFailed() { + ActivityManager.RunningTaskInfo info = mock(ActivityManager.RunningTaskInfo.class); + mImpl.onActivityLaunchOnSecondaryDisplayFailed(info, 1); + verify(mCallback).onActivityLaunchOnSecondaryDisplayFailed(eq(info)); + verify(mOtherCallback).onActivityLaunchOnSecondaryDisplayFailed(eq(info)); + } + + @Test + public void testOnActivityLaunchOnSecondaryDisplayRerouted() { + ActivityManager.RunningTaskInfo info = mock(ActivityManager.RunningTaskInfo.class); + mImpl.onActivityLaunchOnSecondaryDisplayRerouted(info, 1); + verify(mCallback).onActivityLaunchOnSecondaryDisplayRerouted(eq(info)); + verify(mOtherCallback).onActivityLaunchOnSecondaryDisplayRerouted(eq(info)); + } + + @Test + public void testOnActivityRequestedOrientationChanged() { + mImpl.onActivityRequestedOrientationChanged(1, 2); + verify(mCallback).onActivityRequestedOrientationChanged(eq(1), eq(2)); + verify(mOtherCallback).onActivityRequestedOrientationChanged(eq(1), eq(2)); + } + + @Test + public void testOnActivityRotation() { + mImpl.onActivityRotation(123); + verify(mCallback).onActivityRotation(eq(123)); + verify(mOtherCallback).onActivityRotation(eq(123)); + } + + /** + * Handler that synchronously calls TaskStackListenerImpl#handleMessage() when it receives a + * message. + */ + private class ProxyToListenerImplHandler extends Handler { + public ProxyToListenerImplHandler(Callback callback) { + super(callback); + } + + @Override + public boolean sendMessageAtTime(Message msg, long uptimeMillis) { + return mImpl.handleMessage(msg); + } + } +}
\ No newline at end of file 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/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java new file mode 100644 index 000000000000..5821eed6f611 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java @@ -0,0 +1,118 @@ +/* + * 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.split; + +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import android.content.res.Configuration; +import android.graphics.Rect; +import android.view.SurfaceControl; + +import androidx.test.annotation.UiThreadTest; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.internal.policy.DividerSnapAlgorithm; +import com.android.wm.shell.ShellTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Tests for {@link SplitLayout} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SplitLayoutTests extends ShellTestCase { + @Mock SplitLayout.LayoutChangeListener mLayoutChangeListener; + @Mock SurfaceControl mRootLeash; + private SplitLayout mSplitLayout; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mSplitLayout = new SplitLayout( + "TestSplitLayout", + mContext, + getConfiguration(false), + mLayoutChangeListener, + b -> b.setParent(mRootLeash)); + } + + @Test + @UiThreadTest + public void testUpdateConfiguration() { + mSplitLayout.init(); + assertThat(mSplitLayout.updateConfiguration(getConfiguration(false))).isFalse(); + assertThat(mSplitLayout.updateConfiguration(getConfiguration(true))).isTrue(); + } + + @Test + public void testUpdateDivideBounds() { + mSplitLayout.updateDivideBounds(anyInt()); + verify(mLayoutChangeListener).onBoundsChanging(any(SplitLayout.class)); + } + + @Test + public void testSetDividePosition() { + mSplitLayout.setDividePosition(anyInt()); + verify(mLayoutChangeListener).onBoundsChanged(any(SplitLayout.class)); + } + + @Test + public void testOnDoubleTappedDivider() { + mSplitLayout.onDoubleTappedDivider(); + verify(mLayoutChangeListener).onDoubleTappedDivider(); + } + + @Test + @UiThreadTest + public void testSnapToDismissTarget() { + // verify it callbacks properly when the snap target indicates dismissing split. + DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */, + DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START); + mSplitLayout.snapToTarget(0 /* currentPosition */, snapTarget); + verify(mLayoutChangeListener).onSnappedToDismiss(eq(false)); + snapTarget = getSnapTarget(0 /* position */, + DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END); + mSplitLayout.snapToTarget(0 /* currentPosition */, snapTarget); + verify(mLayoutChangeListener).onSnappedToDismiss(eq(true)); + } + + private static Configuration getConfiguration(boolean isLandscape) { + final Configuration configuration = new Configuration(); + configuration.unset(); + configuration.orientation = isLandscape ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT; + configuration.windowConfiguration.setBounds( + new Rect(0, 0, isLandscape ? 2160 : 1080, isLandscape ? 1080 : 2160)); + return configuration; + } + + private static DividerSnapAlgorithm.SnapTarget getSnapTarget(int position, int flag) { + return new DividerSnapAlgorithm.SnapTarget( + position /* position */, position /* taskPosition */, flag); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java new file mode 100644 index 000000000000..698315a77d8e --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java @@ -0,0 +1,67 @@ +/* + * 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.split; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.content.res.Configuration; +import android.graphics.Rect; +import android.view.SurfaceControl; + +import androidx.test.annotation.UiThreadTest; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Tests for {@link SplitWindowManager} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SplitWindowManagerTests extends ShellTestCase { + @Mock SurfaceControl mSurfaceControl; + @Mock SplitLayout mSplitLayout; + private SplitWindowManager mSplitWindowManager; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + final Configuration configuration = new Configuration(); + configuration.setToDefaults(); + mSplitWindowManager = new SplitWindowManager("TestSplitDivider", mContext, configuration, + b -> b.setParent(mSurfaceControl)); + when(mSplitLayout.getDividerBounds()).thenReturn( + new Rect(0, 0, configuration.windowConfiguration.getBounds().width(), + configuration.windowConfiguration.getBounds().height())); + } + + @Test + @UiThreadTest + public void testInitRelease() { + mSplitWindowManager.init(mSplitLayout); + assertThat(mSplitWindowManager.getSurfaceControl()).isNotNull(); + mSplitWindowManager.release(); + assertThat(mSplitWindowManager.getSurfaceControl()).isNull(); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java new file mode 100644 index 000000000000..25721066b713 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java @@ -0,0 +1,347 @@ +/* + * 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.draganddrop; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; +import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY; +import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT; +import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK; + +import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_FULLSCREEN; +import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM; +import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT; +import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT; +import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_TOP; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_UNDEFINED; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; + +import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.fail; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.PendingIntent; +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Insets; +import android.os.RemoteException; +import android.view.DisplayInfo; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.draganddrop.DragAndDropPolicy.Target; +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.ArrayList; +import java.util.Collections; +import java.util.HashSet; + +/** + * Tests for the drag and drop policy. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class DragAndDropPolicyTest { + + @Mock + private Context mContext; + + @Mock + private ActivityTaskManager mActivityTaskManager; + + // Both the split-screen and start interface. + @Mock + private SplitScreen mSplitScreenStarter; + + private DisplayLayout mLandscapeDisplayLayout; + private DisplayLayout mPortraitDisplayLayout; + private Insets mInsets; + private DragAndDropPolicy mPolicy; + + private ClipData mActivityClipData; + private ClipData mNonResizeableActivityClipData; + private ClipData mTaskClipData; + private ClipData mShortcutClipData; + + private ActivityManager.RunningTaskInfo mHomeTask; + private ActivityManager.RunningTaskInfo mFullscreenAppTask; + private ActivityManager.RunningTaskInfo mNonResizeableFullscreenAppTask; + private ActivityManager.RunningTaskInfo mSplitPrimaryAppTask; + + @Before + public void setUp() throws RemoteException { + MockitoAnnotations.initMocks(this); + + Resources res = mock(Resources.class); + Configuration config = new Configuration(); + doReturn(config).when(res).getConfiguration(); + doReturn(res).when(mContext).getResources(); + DisplayInfo info = new DisplayInfo(); + info.logicalWidth = 200; + info.logicalHeight = 100; + mLandscapeDisplayLayout = new DisplayLayout(info, res, false, false); + DisplayInfo info2 = new DisplayInfo(); + info.logicalWidth = 100; + info.logicalHeight = 200; + mPortraitDisplayLayout = new DisplayLayout(info2, res, false, false); + mInsets = Insets.of(0, 0, 0, 0); + + mPolicy = new DragAndDropPolicy( + mContext, mActivityTaskManager, mSplitScreenStarter, mSplitScreenStarter); + mActivityClipData = createClipData(MIMETYPE_APPLICATION_ACTIVITY); + mNonResizeableActivityClipData = createClipData(MIMETYPE_APPLICATION_ACTIVITY); + setClipDataResizeable(mNonResizeableActivityClipData, false); + mTaskClipData = createClipData(MIMETYPE_APPLICATION_TASK); + mShortcutClipData = createClipData(MIMETYPE_APPLICATION_SHORTCUT); + + mHomeTask = createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_HOME); + mFullscreenAppTask = createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); + mNonResizeableFullscreenAppTask = + createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); + mNonResizeableFullscreenAppTask.isResizeable = false; + mSplitPrimaryAppTask = createTaskInfo(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, + ACTIVITY_TYPE_STANDARD); + + setInSplitScreen(false); + setRunningTask(mFullscreenAppTask); + } + + /** + * Creates a clip data that is by default resizeable. + */ + private ClipData createClipData(String mimeType) { + ClipDescription clipDescription = new ClipDescription(mimeType, new String[] { mimeType }); + Intent i = new Intent(); + switch (mimeType) { + case MIMETYPE_APPLICATION_SHORTCUT: + i.putExtra(Intent.EXTRA_PACKAGE_NAME, "package"); + i.putExtra(Intent.EXTRA_SHORTCUT_ID, "shortcut_id"); + break; + case MIMETYPE_APPLICATION_TASK: + i.putExtra(Intent.EXTRA_TASK_ID, 12345); + break; + case MIMETYPE_APPLICATION_ACTIVITY: + i.putExtra(ClipDescription.EXTRA_PENDING_INTENT, mock(PendingIntent.class)); + break; + } + i.putExtra(Intent.EXTRA_USER, android.os.Process.myUserHandle()); + ClipData.Item item = new ClipData.Item(i); + item.setActivityInfo(new ActivityInfo()); + ClipData data = new ClipData(clipDescription, item); + setClipDataResizeable(data, true); + return data; + } + + private ActivityManager.RunningTaskInfo createTaskInfo(int winMode, int actType) { + ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo(); + info.configuration.windowConfiguration.setActivityType(actType); + info.configuration.windowConfiguration.setWindowingMode(winMode); + info.isResizeable = true; + return info; + } + + private void setRunningTask(ActivityManager.RunningTaskInfo task) { + doReturn(Collections.singletonList(task)).when(mActivityTaskManager) + .getTasks(anyInt(), anyBoolean()); + } + + private void setClipDataResizeable(ClipData data, boolean resizeable) { + data.getItemAt(0).getActivityInfo().resizeMode = resizeable + ? ActivityInfo.RESIZE_MODE_RESIZEABLE + : ActivityInfo.RESIZE_MODE_UNRESIZEABLE; + } + + private void setInSplitScreen(boolean inSplitscreen) { + doReturn(inSplitscreen).when(mSplitScreenStarter).isSplitScreenVisible(); + } + + @Test + public void testDragAppOverFullscreenHome_expectOnlyFullscreenTarget() { + setRunningTask(mHomeTask); + mPolicy.start(mLandscapeDisplayLayout, mActivityClipData); + ArrayList<Target> targets = assertExactTargetTypes( + mPolicy.getTargets(mInsets), TYPE_FULLSCREEN); + + mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); + verify(mSplitScreenStarter).startClipDescription(any(), any(), + eq(STAGE_TYPE_UNDEFINED), eq(STAGE_POSITION_UNDEFINED)); + } + + @Test + public void testDragAppOverFullscreenApp_expectSplitScreenAndFullscreenTargets() { + setRunningTask(mFullscreenAppTask); + mPolicy.start(mLandscapeDisplayLayout, mActivityClipData); + ArrayList<Target> targets = assertExactTargetTypes( + mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); + + mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); + verify(mSplitScreenStarter).startClipDescription(any(), any(), + eq(STAGE_TYPE_UNDEFINED), eq(STAGE_POSITION_UNDEFINED)); + reset(mSplitScreenStarter); + + mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), mActivityClipData); + verify(mSplitScreenStarter).startClipDescription(any(), any(), + eq(STAGE_TYPE_SIDE), eq(STAGE_POSITION_BOTTOM_OR_RIGHT)); + } + + @Test + public void testDragAppOverFullscreenAppPhone_expectVerticalSplitScreenAndFullscreenTargets() { + setRunningTask(mFullscreenAppTask); + mPolicy.start(mPortraitDisplayLayout, mActivityClipData); + ArrayList<Target> targets = assertExactTargetTypes( + mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM); + + mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); + verify(mSplitScreenStarter).startClipDescription(any(), any(), + eq(STAGE_TYPE_UNDEFINED), eq(STAGE_POSITION_UNDEFINED)); + reset(mSplitScreenStarter); + + mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), mActivityClipData); + verify(mSplitScreenStarter).startClipDescription(any(), any(), + eq(STAGE_TYPE_SIDE), eq(STAGE_POSITION_BOTTOM_OR_RIGHT)); + } + + @Test + public void testDragAppOverFullscreenNonResizeableApp_expectOnlyFullscreenTargets() { + setRunningTask(mNonResizeableFullscreenAppTask); + mPolicy.start(mLandscapeDisplayLayout, mActivityClipData); + ArrayList<Target> targets = assertExactTargetTypes( + mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); + + mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); + verify(mSplitScreenStarter).startClipDescription(any(), any(), + eq(STAGE_TYPE_UNDEFINED), eq(STAGE_POSITION_UNDEFINED)); + } + + @Test + public void testDragNonResizeableAppOverFullscreenApp_expectOnlyFullscreenTargets() { + setRunningTask(mFullscreenAppTask); + mPolicy.start(mLandscapeDisplayLayout, mNonResizeableActivityClipData); + ArrayList<Target> targets = assertExactTargetTypes( + mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); + + mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); + verify(mSplitScreenStarter).startClipDescription(any(), any(), + eq(STAGE_TYPE_UNDEFINED), eq(STAGE_POSITION_UNDEFINED)); + } + + @Test + public void testDragAppOverSplitApp_expectFullscreenAndSplitTargets() { + setInSplitScreen(true); + setRunningTask(mSplitPrimaryAppTask); + mPolicy.start(mLandscapeDisplayLayout, mActivityClipData); + ArrayList<Target> targets = assertExactTargetTypes( + mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); + + mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); + verify(mSplitScreenStarter).startClipDescription(any(), any(), + eq(STAGE_TYPE_UNDEFINED), eq(STAGE_POSITION_UNDEFINED)); + reset(mSplitScreenStarter); + + // TODO(b/169894807): Just verify starting for the non-docked task until we have app pairs + mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), mActivityClipData); + verify(mSplitScreenStarter).startClipDescription(any(), any(), + eq(STAGE_TYPE_SIDE), eq(STAGE_POSITION_BOTTOM_OR_RIGHT)); + } + + @Test + public void testDragAppOverSplitAppPhone_expectFullscreenAndVerticalSplitTargets() { + setInSplitScreen(true); + setRunningTask(mSplitPrimaryAppTask); + mPolicy.start(mPortraitDisplayLayout, mActivityClipData); + ArrayList<Target> targets = assertExactTargetTypes( + mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM); + + mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); + verify(mSplitScreenStarter).startClipDescription(any(), any(), + eq(STAGE_TYPE_UNDEFINED), eq(STAGE_POSITION_UNDEFINED)); + reset(mSplitScreenStarter); + + // TODO(b/169894807): Just verify starting for the non-docked task until we have app pairs + mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), mActivityClipData); + verify(mSplitScreenStarter).startClipDescription(any(), any(), + eq(STAGE_TYPE_SIDE), eq(STAGE_POSITION_BOTTOM_OR_RIGHT)); + } + + @Test + public void testTargetHitRects() { + setRunningTask(mFullscreenAppTask); + mPolicy.start(mLandscapeDisplayLayout, mActivityClipData); + ArrayList<Target> targets = mPolicy.getTargets(mInsets); + for (Target t : targets) { + assertTrue(mPolicy.getTargetAtLocation(t.hitRegion.left, t.hitRegion.top) == t); + assertTrue(mPolicy.getTargetAtLocation(t.hitRegion.right - 1, t.hitRegion.top) == t); + assertTrue(mPolicy.getTargetAtLocation(t.hitRegion.right - 1, t.hitRegion.bottom - 1) + == t); + assertTrue(mPolicy.getTargetAtLocation(t.hitRegion.left, t.hitRegion.bottom - 1) + == t); + } + } + + private Target filterTargetByType(ArrayList<Target> targets, int type) { + for (Target t : targets) { + if (type == t.type) { + return t; + } + } + fail("Target with type: " + type + " not found"); + return null; + } + + private ArrayList<Target> assertExactTargetTypes(ArrayList<Target> targets, + int... expectedTargetTypes) { + HashSet<Integer> expected = new HashSet<>(); + for (int t : expectedTargetTypes) { + expected.add(t); + } + for (Target t : targets) { + if (!expected.contains(t.type)) { + fail("Found unexpected target type: " + t.type); + } + expected.remove(t.type); + } + assertTrue(expected.isEmpty()); + return targets; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutControllerTest.java new file mode 100644 index 000000000000..f10dc16fae5c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutControllerTest.java @@ -0,0 +1,77 @@ +/* + * 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.hidedisplaycutout; + +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; + +import android.platform.test.annotations.Presubmit; +import android.testing.AndroidTestingRunner; +import android.testing.TestableContext; +import android.testing.TestableLooper; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.ShellExecutor; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@Presubmit +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class HideDisplayCutoutControllerTest { + private TestableContext mContext = new TestableContext( + InstrumentationRegistry.getInstrumentation().getTargetContext(), null); + + private HideDisplayCutoutController mHideDisplayCutoutController; + @Mock + private HideDisplayCutoutOrganizer mMockDisplayAreaOrganizer; + @Mock + private ShellExecutor mMockMainExecutor; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mHideDisplayCutoutController = new HideDisplayCutoutController( + mContext, mMockDisplayAreaOrganizer, mMockMainExecutor); + } + + @Test + public void testToggleHideDisplayCutout_On() { + mHideDisplayCutoutController.mEnabled = false; + mContext.getOrCreateTestableResources().addOverride( + com.android.internal.R.bool.config_hideDisplayCutoutWithDisplayArea, true); + reset(mMockDisplayAreaOrganizer); + mHideDisplayCutoutController.updateStatus(); + verify(mMockDisplayAreaOrganizer).enableHideDisplayCutout(); + } + + @Test + public void testToggleHideDisplayCutout_Off() { + mHideDisplayCutoutController.mEnabled = true; + mContext.getOrCreateTestableResources().addOverride( + com.android.internal.R.bool.config_hideDisplayCutoutWithDisplayArea, false); + mHideDisplayCutoutController.updateStatus(); + verify(mMockDisplayAreaOrganizer).disableHideDisplayCutout(); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizerTest.java new file mode 100644 index 000000000000..963757045453 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizerTest.java @@ -0,0 +1,222 @@ +/* + * 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.hidedisplaycutout; + +import static android.view.Display.DEFAULT_DISPLAY; +import static android.window.DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.res.Configuration; +import android.graphics.Insets; +import android.graphics.Rect; +import android.os.Binder; +import android.platform.test.annotations.Presubmit; +import android.testing.AndroidTestingRunner; +import android.testing.TestableContext; +import android.testing.TestableLooper; +import android.view.Display; +import android.view.Surface; +import android.view.SurfaceControl; +import android.window.DisplayAreaAppearedInfo; +import android.window.DisplayAreaInfo; +import android.window.DisplayAreaOrganizer; +import android.window.IWindowContainerToken; +import android.window.WindowContainerToken; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +import com.android.internal.R; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; + +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; + +import java.util.ArrayList; + +@Presubmit +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class HideDisplayCutoutOrganizerTest { + private TestableContext mContext = new TestableContext( + InstrumentationRegistry.getInstrumentation().getTargetContext(), null); + + @Mock + private DisplayController mMockDisplayController; + private HideDisplayCutoutOrganizer mOrganizer; + + @Mock + private ShellExecutor mMockMainExecutor; + + private DisplayAreaInfo mDisplayAreaInfo; + private SurfaceControl mLeash; + + @Mock + private Display mDisplay; + @Mock + private IWindowContainerToken mMockRealToken; + private WindowContainerToken mToken; + + private final Rect mFakeDefaultBounds = new Rect(0, 0, 100, 200); + private final Insets mFakeDefaultCutoutInsets = Insets.of(0, 10, 0, 0); + private final int mFakeStatusBarHeightPortrait = 15; + private final int mFakeStatusBarHeightLandscape = 10; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + when(mMockDisplayController.getDisplay(anyInt())).thenReturn(mDisplay); + + HideDisplayCutoutOrganizer organizer = new HideDisplayCutoutOrganizer( + mContext, mMockDisplayController, mMockMainExecutor); + mOrganizer = Mockito.spy(organizer); + doNothing().when(mOrganizer).unregisterOrganizer(); + doNothing().when(mOrganizer).applyBoundsAndOffsets(any(), any(), any(), any()); + doNothing().when(mOrganizer).applyTransaction(any(), any()); + + // It will be called when mDisplayAreaMap.containKey(token) is called. + Binder binder = new Binder(); + doReturn(binder).when(mMockRealToken).asBinder(); + mToken = new WindowContainerToken(mMockRealToken); + mLeash = new SurfaceControl(); + mDisplayAreaInfo = new DisplayAreaInfo( + mToken, DEFAULT_DISPLAY, FEATURE_HIDE_DISPLAY_CUTOUT); + mDisplayAreaInfo.configuration.orientation = Configuration.ORIENTATION_PORTRAIT; + DisplayAreaAppearedInfo info = new DisplayAreaAppearedInfo(mDisplayAreaInfo, mLeash); + ArrayList<DisplayAreaAppearedInfo> infoList = new ArrayList<>(); + infoList.add(info); + doReturn(infoList).when(mOrganizer).registerOrganizer( + DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT); + } + + @Test + public void testEnableHideDisplayCutout() { + mOrganizer.enableHideDisplayCutout(); + + verify(mOrganizer).registerOrganizer(DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT); + verify(mOrganizer).addDisplayAreaInfoAndLeashToMap(mDisplayAreaInfo, mLeash); + verify(mOrganizer).updateBoundsAndOffsets(true); + assertThat(mOrganizer.mDisplayAreaMap.containsKey(mDisplayAreaInfo.token)).isTrue(); + assertThat(mOrganizer.mDisplayAreaMap.containsValue(mLeash)).isTrue(); + } + + @Test + public void testOnDisplayAreaAppeared() { + mOrganizer.onDisplayAreaAppeared(mDisplayAreaInfo, mLeash); + + assertThat(mOrganizer.mDisplayAreaMap.containsKey(mToken)).isTrue(); + assertThat(mOrganizer.mDisplayAreaMap.containsValue(mLeash)).isTrue(); + } + + @Test + public void testOnDisplayAreaVanished() { + mOrganizer.mDisplayAreaMap.put(mDisplayAreaInfo.token, mLeash); + mOrganizer.onDisplayAreaVanished(mDisplayAreaInfo); + + assertThat(mOrganizer.mDisplayAreaMap.containsKey(mDisplayAreaInfo.token)).isFalse(); + } + + @Test + public void testToggleHideDisplayCutout_enable_rot0() { + doReturn(mFakeDefaultBounds).when(mOrganizer).getDisplayBoundsOfNaturalOrientation(); + doReturn(mFakeDefaultCutoutInsets).when(mOrganizer) + .getDisplayCutoutInsetsOfNaturalOrientation(); + mContext.getOrCreateTestableResources().addOverride( + R.dimen.status_bar_height_portrait, mFakeStatusBarHeightPortrait); + doReturn(Surface.ROTATION_0).when(mDisplay).getRotation(); + mOrganizer.enableHideDisplayCutout(); + + verify(mOrganizer).registerOrganizer(DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT); + verify(mOrganizer).addDisplayAreaInfoAndLeashToMap(mDisplayAreaInfo, mLeash); + verify(mOrganizer).updateBoundsAndOffsets(true); + assertThat(mOrganizer.mCurrentDisplayBounds).isEqualTo(new Rect(0, 15, 100, 200)); + assertThat(mOrganizer.mOffsetX).isEqualTo(0); + assertThat(mOrganizer.mOffsetY).isEqualTo(15); + assertThat(mOrganizer.mRotation).isEqualTo(Surface.ROTATION_0); + } + + @Test + public void testToggleHideDisplayCutout_enable_rot90() { + doReturn(mFakeDefaultBounds).when(mOrganizer).getDisplayBoundsOfNaturalOrientation(); + doReturn(mFakeDefaultCutoutInsets).when(mOrganizer) + .getDisplayCutoutInsetsOfNaturalOrientation(); + mContext.getOrCreateTestableResources().addOverride( + R.dimen.status_bar_height_landscape, mFakeStatusBarHeightLandscape); + doReturn(Surface.ROTATION_90).when(mDisplay).getRotation(); + mOrganizer.enableHideDisplayCutout(); + + verify(mOrganizer).registerOrganizer(DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT); + verify(mOrganizer).addDisplayAreaInfoAndLeashToMap(mDisplayAreaInfo, mLeash); + verify(mOrganizer).updateBoundsAndOffsets(true); + assertThat(mOrganizer.mCurrentDisplayBounds).isEqualTo(new Rect(10, 0, 200, 100)); + assertThat(mOrganizer.mOffsetX).isEqualTo(10); + assertThat(mOrganizer.mOffsetY).isEqualTo(0); + assertThat(mOrganizer.mRotation).isEqualTo(Surface.ROTATION_90); + } + + @Test + public void testToggleHideDisplayCutout_enable_rot270() { + doReturn(mFakeDefaultBounds).when(mOrganizer).getDisplayBoundsOfNaturalOrientation(); + doReturn(mFakeDefaultCutoutInsets).when(mOrganizer) + .getDisplayCutoutInsetsOfNaturalOrientation(); + mContext.getOrCreateTestableResources().addOverride( + R.dimen.status_bar_height_landscape, mFakeStatusBarHeightLandscape); + doReturn(Surface.ROTATION_270).when(mDisplay).getRotation(); + mOrganizer.enableHideDisplayCutout(); + + verify(mOrganizer).registerOrganizer(DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT); + verify(mOrganizer).addDisplayAreaInfoAndLeashToMap(mDisplayAreaInfo, mLeash); + verify(mOrganizer).updateBoundsAndOffsets(true); + assertThat(mOrganizer.mCurrentDisplayBounds).isEqualTo(new Rect(0, 0, 190, 100)); + assertThat(mOrganizer.mOffsetX).isEqualTo(0); + assertThat(mOrganizer.mOffsetY).isEqualTo(0); + assertThat(mOrganizer.mRotation).isEqualTo(Surface.ROTATION_270); + } + + @Test + public void testToggleHideDisplayCutout_disable() { + doReturn(mFakeDefaultBounds).when(mOrganizer).getDisplayBoundsOfNaturalOrientation(); + doReturn(mFakeDefaultCutoutInsets).when(mOrganizer) + .getDisplayCutoutInsetsOfNaturalOrientation(); + mContext.getOrCreateTestableResources().addOverride( + R.dimen.status_bar_height_portrait, mFakeStatusBarHeightPortrait); + mOrganizer.enableHideDisplayCutout(); + + // disable hide display cutout + mOrganizer.disableHideDisplayCutout(); + verify(mOrganizer).updateBoundsAndOffsets(false); + verify(mOrganizer).unregisterOrganizer(); + assertThat(mOrganizer.mCurrentDisplayBounds).isEqualTo(new Rect(0, 0, 0, 0)); + assertThat(mOrganizer.mOffsetX).isEqualTo(0); + assertThat(mOrganizer.mOffsetY).isEqualTo(0); + } +} 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..8d5139b182f0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedAnimationControllerTest.java @@ -0,0 +1,79 @@ +/* + * 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 android.window.WindowContainerToken; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.ShellExecutor; + +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; + @Mock + private WindowContainerToken mMockToken; + + @Mock + private ShellExecutor mMainExecutor; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mTutorialHandler = new OneHandedTutorialHandler(mContext, mMainExecutor); + 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(mMockToken, mMockLeash, originalBounds, destinationBounds); + + assertNotNull(animator); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedBackgroundPanelOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedBackgroundPanelOrganizerTest.java new file mode 100644 index 000000000000..e9c4af12a0d6 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedBackgroundPanelOrganizerTest.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.onehanded; + +import static android.view.Display.DEFAULT_DISPLAY; +import static android.window.DisplayAreaOrganizer.FEATURE_ONE_HANDED_BACKGROUND_PANEL; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.Display; +import android.view.SurfaceControl; +import android.window.DisplayAreaInfo; +import android.window.IWindowContainerToken; +import android.window.WindowContainerToken; + +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 OneHandedBackgroundPanelOrganizerTest extends OneHandedTestCase { + private DisplayAreaInfo mDisplayAreaInfo; + private Display mDisplay; + private OneHandedBackgroundPanelOrganizer mBackgroundPanelOrganizer; + private WindowContainerToken mToken; + private SurfaceControl mLeash; + private TestableLooper mTestableLooper; + + @Mock + IWindowContainerToken mMockRealToken; + @Mock + DisplayController mMockDisplayController; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mTestableLooper = TestableLooper.get(this); + mToken = new WindowContainerToken(mMockRealToken); + mLeash = new SurfaceControl(); + mDisplay = mContext.getDisplay(); + when(mMockDisplayController.getDisplay(anyInt())).thenReturn(mDisplay); + mDisplayAreaInfo = new DisplayAreaInfo(mToken, DEFAULT_DISPLAY, + FEATURE_ONE_HANDED_BACKGROUND_PANEL); + + mBackgroundPanelOrganizer = new OneHandedBackgroundPanelOrganizer(mContext, + mMockDisplayController, Runnable::run); + } + + @Test + public void testOnDisplayAreaAppeared() { + mBackgroundPanelOrganizer.onDisplayAreaAppeared(mDisplayAreaInfo, mLeash); + mTestableLooper.processAllMessages(); + + assertThat(mBackgroundPanelOrganizer.getBackgroundSurface()).isNotNull(); + } + + @Test + public void testUnregisterOrganizer() { + mBackgroundPanelOrganizer.onDisplayAreaAppeared(mDisplayAreaInfo, mLeash); + mTestableLooper.processAllMessages(); + mBackgroundPanelOrganizer.unregisterOrganizer(); + + assertThat(mBackgroundPanelOrganizer.getBackgroundSurface()).isNull(); + } + + @Test + public void testShowBackgroundLayer() { + mBackgroundPanelOrganizer.onDisplayAreaAppeared(mDisplayAreaInfo, mLeash); + mBackgroundPanelOrganizer.showBackgroundPanelLayer(); + mTestableLooper.processAllMessages(); + + assertThat(mBackgroundPanelOrganizer.mIsShowing).isTrue(); + } + + @Test + public void testRemoveBackgroundLayer() { + mBackgroundPanelOrganizer.onDisplayAreaAppeared(mDisplayAreaInfo, mLeash); + mBackgroundPanelOrganizer.removeBackgroundPanelLayer(); + mTestableLooper.processAllMessages(); + + assertThat(mBackgroundPanelOrganizer.mIsShowing).isFalse(); + } +} 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..f141167178a1 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java @@ -0,0 +1,211 @@ +/* + * 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.os.Handler; +import android.provider.Settings; +import android.testing.AndroidTestingRunner; +import android.view.Display; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TaskStackListenerImpl; + +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) +public class OneHandedControllerTest extends OneHandedTestCase { + Display mDisplay; + OneHandedController mOneHandedController; + OneHandedTimeoutHandler mTimeoutHandler; + + @Mock + DisplayController mMockDisplayController; + @Mock + OneHandedBackgroundPanelOrganizer mMockBackgroundOrganizer; + @Mock + OneHandedDisplayAreaOrganizer mMockDisplayAreaOrganizer; + @Mock + OneHandedTouchHandler mMockTouchHandler; + @Mock + OneHandedTutorialHandler mMockTutorialHandler; + @Mock + OneHandedGestureHandler mMockGestureHandler; + @Mock + OneHandedTimeoutHandler mMockTimeoutHandler; + @Mock + OneHandedUiEventLogger mMockUiEventLogger; + @Mock + IOverlayManager mMockOverlayManager; + @Mock + TaskStackListenerImpl mMockTaskStackListener; + @Mock + ShellExecutor mMockShellMainExecutor; + @Mock + Handler mMockShellMainHandler; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mDisplay = mContext.getDisplay(); + mTimeoutHandler = Mockito.spy(new OneHandedTimeoutHandler(mMockShellMainExecutor)); + OneHandedController oneHandedController = new OneHandedController( + mContext, + mMockDisplayController, + mMockBackgroundOrganizer, + mMockDisplayAreaOrganizer, + mMockTouchHandler, + mMockTutorialHandler, + mMockGestureHandler, + mTimeoutHandler, + mMockUiEventLogger, + mMockOverlayManager, + mMockTaskStackListener, + mMockShellMainExecutor, + mMockShellMainHandler); + mOneHandedController = Mockito.spy(oneHandedController); + + 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, + mMockBackgroundOrganizer, mMockShellMainExecutor); + + 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..01162b5c0b83 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java @@ -0,0 +1,281 @@ +/* + * 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.doReturn; +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.Binder; +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 com.android.wm.shell.common.ShellExecutor; + +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) +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; + @Mock + OneHandedBackgroundPanelOrganizer mMockBackgroundOrganizer; + @Mock + ShellExecutor mMockShellMainExecutor; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mTestableLooper = TestableLooper.get(this); + Binder binder = new Binder(); + doReturn(binder).when(mMockRealToken).asBinder(); + 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(), 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.addOneHandedAnimationCallback(any())).thenReturn(mFakeAnimator); + when(mMockAnimator.setTransitionDirection(anyInt())).thenReturn(mFakeAnimator); + when(mMockLeash.getWidth()).thenReturn(DISPLAY_WIDTH); + when(mMockLeash.getHeight()).thenReturn(DISPLAY_HEIGHT); + + mDisplayAreaOrganizer = spy(new OneHandedDisplayAreaOrganizer(mContext, + mMockDisplayController, + mMockAnimationController, + mTutorialHandler, + mMockBackgroundOrganizer, + mMockShellMainExecutor)); + } + + @Test + public void testOnDisplayAreaAppeared() { + mDisplayAreaOrganizer.onDisplayAreaAppeared(mDisplayAreaInfo, mLeash); + + verify(mMockAnimationController, never()).getAnimator(any(), any(), any(), any()); + } + + @Test + public void testOnDisplayAreaVanished() { + mDisplayAreaOrganizer.onDisplayAreaAppeared(mDisplayAreaInfo, mLeash); + mDisplayAreaOrganizer.onDisplayAreaVanished(mDisplayAreaInfo); + + assertThat(mDisplayAreaOrganizer.mDisplayAreaTokenMap).isEmpty(); + } + + @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); + verify(mDisplayAreaOrganizer).finishOffset(anyInt(), anyInt()); + } + + @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); + verify(mDisplayAreaOrganizer).finishOffset(anyInt(), anyInt()); + } + + @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); + verify(mDisplayAreaOrganizer).finishOffset(anyInt(), anyInt()); + } + + @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); + verify(mDisplayAreaOrganizer).finishOffset(anyInt(), anyInt()); + } + + @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); + verify(mDisplayAreaOrganizer).finishOffset(anyInt(), anyInt()); + } + + @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); + verify(mDisplayAreaOrganizer).finishOffset(anyInt(), anyInt()); + } + + @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); + verify(mDisplayAreaOrganizer).finishOffset(anyInt(), anyInt()); + } + + @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); + verify(mDisplayAreaOrganizer).finishOffset(anyInt(), anyInt()); + } + + @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); + verify(mDisplayAreaOrganizer, never()).finishOffset(anyInt(), anyInt()); + } + + @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); + verify(mDisplayAreaOrganizer, never()).finishOffset(anyInt(), anyInt()); + } + + @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); + verify(mDisplayAreaOrganizer, never()).finishOffset(anyInt(), anyInt()); + } + + @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); + verify(mDisplayAreaOrganizer, never()).finishOffset(anyInt(), anyInt()); + } + + @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); + verify(mDisplayAreaOrganizer, never()).finishOffset(anyInt(), anyInt()); + } + + @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); + verify(mDisplayAreaOrganizer, never()).finishOffset(anyInt(), anyInt()); + } + + @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); + verify(mDisplayAreaOrganizer, never()).finishOffset(anyInt(), anyInt()); + } + + @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); + verify(mDisplayAreaOrganizer, never()).finishOffset(anyInt(), anyInt()); + } +} 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..e5f2ff717e37 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedGestureHandlerTest.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.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 com.android.wm.shell.common.ShellExecutor; + +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) +public class OneHandedGestureHandlerTest extends OneHandedTestCase { + OneHandedTutorialHandler mTutorialHandler; + OneHandedGestureHandler mGestureHandler; + @Mock + DisplayController mMockDisplayController; + @Mock + ShellExecutor mMockShellMainExecutor; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mTutorialHandler = new OneHandedTutorialHandler(mContext, mMockShellMainExecutor); + mGestureHandler = new OneHandedGestureHandler(mContext, mMockDisplayController, + mMockShellMainExecutor); + } + + @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..f8c9d535ba94 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedSettingsUtilTest.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.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) +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..73a95345e1c9 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java @@ -0,0 +1,106 @@ +/* + * 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() { + assumeTrue(SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false)); + + 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); + } + + @After + public void restoreSettings() { + if (mContext == null) { + // Return early if one-handed mode is not supported + return; + } + + 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..bbe8891817d6 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandlerTest.java @@ -0,0 +1,108 @@ +/* + * 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 static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.verify; + +import android.os.Looper; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.ShellExecutor; + +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; + +import java.util.ArrayList; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class OneHandedTimeoutHandlerTest extends OneHandedTestCase { + private OneHandedTimeoutHandler mTimeoutHandler; + private TestShellExecutor mMainExecutor; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mMainExecutor = new TestShellExecutor(); + mTimeoutHandler = Mockito.spy(new OneHandedTimeoutHandler(mMainExecutor)); + } + + @Test + public void testTimeoutHandler_getTimeout_defaultMedium() { + assertThat(mTimeoutHandler.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(); + assertTrue(mTimeoutHandler.hasScheduledTimeout()); + } + + @Test + public void testSetTimeoutNever_neverResetTimer() { + mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_NEVER); + assertFalse(mTimeoutHandler.hasScheduledTimeout()); + } + + @Test + public void testSetTimeoutShort() { + mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS); + verify(mTimeoutHandler).resetTimer(); + assertThat(mTimeoutHandler.getTimeout()).isEqualTo(ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS); + assertTrue(mTimeoutHandler.hasScheduledTimeout()); + } + + @Test + public void testSetTimeoutMedium() { + mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS); + verify(mTimeoutHandler).resetTimer(); + assertThat(mTimeoutHandler.getTimeout()).isEqualTo(ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS); + assertTrue(mTimeoutHandler.hasScheduledTimeout()); + } + + @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() { + mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_LONG_IN_SECONDS); + mTimeoutHandler.resetTimer(); + assertTrue(mTimeoutHandler.hasScheduledTimeout()); + } +} 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..d3b02caf8b65 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTouchHandlerTest.java @@ -0,0 +1,77 @@ +/* + * 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 com.android.wm.shell.common.ShellExecutor; + +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) +public class OneHandedTouchHandlerTest extends OneHandedTestCase { + private OneHandedTouchHandler mTouchHandler; + + @Mock + private OneHandedTimeoutHandler mTimeoutHandler; + + @Mock + private ShellExecutor mMainExecutor; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mTouchHandler = new OneHandedTouchHandler(mTimeoutHandler, mMainExecutor); + } + + @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..c3e6bf376bda --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTutorialHandlerTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.onehanded; + +import static org.mockito.Mockito.verify; + +import android.content.om.IOverlayManager; +import android.os.Handler; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TaskStackListenerImpl; + +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) +public class OneHandedTutorialHandlerTest extends OneHandedTestCase { + @Mock + OneHandedTouchHandler mTouchHandler; + OneHandedTutorialHandler mTutorialHandler; + OneHandedGestureHandler mGestureHandler; + OneHandedTimeoutHandler mTimeoutHandler; + OneHandedController mOneHandedController; + @Mock + DisplayController mMockDisplayController; + @Mock + OneHandedBackgroundPanelOrganizer mMockBackgroundOrganizer; + @Mock + OneHandedDisplayAreaOrganizer mMockDisplayAreaOrganizer; + @Mock + IOverlayManager mMockOverlayManager; + @Mock + TaskStackListenerImpl mMockTaskStackListener; + @Mock + ShellExecutor mMockShellMainExecutor; + @Mock + Handler mMockShellMainHandler; + @Mock + OneHandedUiEventLogger mMockUiEventLogger; + + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mTutorialHandler = new OneHandedTutorialHandler(mContext, mMockShellMainExecutor); + mTimeoutHandler = new OneHandedTimeoutHandler(mMockShellMainExecutor); + mGestureHandler = new OneHandedGestureHandler(mContext, mMockDisplayController, + mMockShellMainExecutor); + mOneHandedController = new OneHandedController( + getContext(), + mMockDisplayController, + mMockBackgroundOrganizer, + mMockDisplayAreaOrganizer, + mTouchHandler, + mTutorialHandler, + mGestureHandler, + mTimeoutHandler, + mMockUiEventLogger, + mMockOverlayManager, + mMockTaskStackListener, + mMockShellMainExecutor, + mMockShellMainHandler); + } + + @Test + public void testRegisterForDisplayAreaOrganizer() { + verify(mMockDisplayAreaOrganizer).registerTransitionCallback(mTutorialHandler); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedUiEventLoggerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedUiEventLoggerTest.java new file mode 100644 index 000000000000..e29fc6a91933 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedUiEventLoggerTest.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 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 OneHandedUiEventLoggerTest 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(); + } + + @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 + {OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_GESTURE_IN, + "writeEvent one_handed_trigger_gesture_in"}, + {OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_GESTURE_OUT, + "writeEvent one_handed_trigger_gesture_out"}, + {OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_OVERSPACE_OUT, + "writeEvent one_handed_trigger_overspace_out"}, + {OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_POP_IME_OUT, + "writeEvent one_handed_trigger_pop_ime_out"}, + {OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT, + "writeEvent one_handed_trigger_rotation_out"}, + {OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_APP_TAPS_OUT, + "writeEvent one_handed_trigger_app_taps_out"}, + {OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_TIMEOUT_OUT, + "writeEvent one_handed_trigger_timeout_out"}, + {OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_SCREEN_OFF_OUT, + "writeEvent one_handed_trigger_screen_off_out"}, + // Settings toggles + {OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_ENABLED_ON, + "writeEvent one_handed_settings_enabled_on"}, + {OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_ENABLED_OFF, + "writeEvent one_handed_settings_enabled_off"}, + {OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_ON, + "writeEvent one_handed_settings_app_taps_exit_on"}, + {OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_OFF, + "writeEvent one_handed_settings_app_taps_exit_off"}, + {OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_ON, + "writeEvent one_handed_settings_timeout_exit_on"}, + {OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_OFF, + "writeEvent one_handed_settings_timeout_exit_off"}, + {OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_NEVER, + "writeEvent one_handed_settings_timeout_seconds_never"}, + {OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_4, + "writeEvent one_handed_settings_timeout_seconds_4"}, + {OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_8, + "writeEvent one_handed_settings_timeout_seconds_8"}, + {OneHandedUiEventLogger.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/pip/PipAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java new file mode 100644 index 000000000000..c565a4cc2e28 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.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; + +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.ShellTestCase; + +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 ShellTestCase { + + 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(), new Rect(), null, + TRANSITION_DIRECTION_TO_PIP, 0); + + assertEquals("Expect ANIM_TYPE_BOUNDS animation", + animator.getAnimationType(), PipAnimationController.ANIM_TYPE_BOUNDS); + } + + @Test + public void getAnimator_whenSameTypeRunning_updateExistingAnimator() { + final Rect baseValue = new Rect(0, 0, 100, 100); + 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, baseValue, startValue, endValue1, null, + TRANSITION_DIRECTION_TO_PIP, 0); + oldAnimator.setSurfaceControlTransactionFactory(DummySurfaceControlTx::new); + oldAnimator.start(); + + final PipAnimationController.PipTransitionAnimator newAnimator = mPipAnimationController + .getAnimator(mLeash, baseValue, startValue, endValue2, null, + TRANSITION_DIRECTION_TO_PIP, 0); + + 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 baseValue = new Rect(0, 0, 100, 100); + 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, baseValue, startValue, endValue1, null, + TRANSITION_DIRECTION_TO_PIP, 0); + + animator.updateEndValue(endValue2); + + assertEquals("updateEndValue updates end value", animator.getEndValue(), endValue2); + } + + @Test + public void pipTransitionAnimator_setPipAnimationCallback() { + final Rect baseValue = new Rect(0, 0, 100, 100); + 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, baseValue, startValue, endValue, null, + TRANSITION_DIRECTION_TO_PIP, 0); + 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 SurfaceControl.Transaction setFrameTimelineVsync(long frameTimelineVsyncId) { + return this; + } + + @Override + public void apply() {} + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java new file mode 100644 index 000000000000..babfc5ca20cf --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java @@ -0,0 +1,432 @@ +/* + * 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.assertTrue; + +import android.graphics.Rect; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.testing.TestableResources; +import android.util.Size; +import android.view.Display; +import android.view.DisplayInfo; +import android.view.Gravity; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.DisplayLayout; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Unit tests against {@link PipBoundsAlgorithm}, 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 PipBoundsAlgorithmTest extends ShellTestCase { + 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 int DEFAULT_MIN_EDGE_SIZE = 100; + + private PipBoundsAlgorithm mPipBoundsAlgorithm; + private DisplayInfo mDefaultDisplayInfo; + private PipBoundsState mPipBoundsState; + + @Before + public void setUp() throws Exception { + initializeMockResources(); + mPipBoundsState = new PipBoundsState(mContext); + mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState); + + mPipBoundsState.setDisplayLayout( + new DisplayLayout(mDefaultDisplayInfo, mContext.getResources(), true, true)); + } + + 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, + DEFAULT_MIN_EDGE_SIZE); + 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, mPipBoundsAlgorithm.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); + + mPipBoundsAlgorithm.onConfigurationChanged(mContext); + + assertEquals("Default aspect ratio should be reloaded", + mPipBoundsAlgorithm.getDefaultAspectRatio(), newDefaultAspectRatio, + ASPECT_RATIO_ERROR_MARGIN); + } + + @Test + public void getDefaultBounds_noOverrideMinSize_matchesDefaultSizeAndAspectRatio() { + final Size defaultSize = mPipBoundsAlgorithm.getSizeForAspectRatio(DEFAULT_ASPECT_RATIO, + DEFAULT_MIN_EDGE_SIZE, mDefaultDisplayInfo.logicalWidth, + mDefaultDisplayInfo.logicalHeight); + + mPipBoundsState.setOverrideMinSize(null); + final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds(); + + assertEquals(defaultSize, new Size(defaultBounds.width(), defaultBounds.height())); + assertEquals(DEFAULT_ASPECT_RATIO, getRectAspectRatio(defaultBounds), + ASPECT_RATIO_ERROR_MARGIN); + } + + @Test + public void getDefaultBounds_widerOverrideMinSize_matchesMinSizeWidthAndDefaultAspectRatio() { + overrideDefaultAspectRatio(1.0f); + // The min size's aspect ratio is greater than the default aspect ratio. + final Size overrideMinSize = new Size(150, 120); + + mPipBoundsState.setOverrideMinSize(overrideMinSize); + final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds(); + + // The default aspect ratio should trump the min size aspect ratio. + assertEquals(DEFAULT_ASPECT_RATIO, getRectAspectRatio(defaultBounds), + ASPECT_RATIO_ERROR_MARGIN); + // The width of the min size is still used with the default aspect ratio. + assertEquals(overrideMinSize.getWidth(), defaultBounds.width()); + } + + @Test + public void getDefaultBounds_tallerOverrideMinSize_matchesMinSizeHeightAndDefaultAspectRatio() { + overrideDefaultAspectRatio(1.0f); + // The min size's aspect ratio is greater than the default aspect ratio. + final Size overrideMinSize = new Size(120, 150); + + mPipBoundsState.setOverrideMinSize(overrideMinSize); + final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds(); + + // The default aspect ratio should trump the min size aspect ratio. + assertEquals(DEFAULT_ASPECT_RATIO, getRectAspectRatio(defaultBounds), + ASPECT_RATIO_ERROR_MARGIN); + // The height of the min size is still used with the default aspect ratio. + assertEquals(overrideMinSize.getHeight(), defaultBounds.height()); + } + + @Test + public void getDefaultBounds_imeShowing_offsetByImeHeight() { + final int imeHeight = 30; + mPipBoundsState.setImeVisibility(false, 0); + final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds(); + + mPipBoundsState.setImeVisibility(true, imeHeight); + final Rect defaultBoundsWithIme = mPipBoundsAlgorithm.getDefaultBounds(); + + assertEquals(imeHeight, defaultBounds.top - defaultBoundsWithIme.top); + } + + @Test + public void getDefaultBounds_shelfShowing_offsetByShelfHeight() { + final int shelfHeight = 30; + mPipBoundsState.setShelfVisibility(false, 0); + final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds(); + + mPipBoundsState.setShelfVisibility(true, shelfHeight); + final Rect defaultBoundsWithShelf = mPipBoundsAlgorithm.getDefaultBounds(); + + assertEquals(shelfHeight, defaultBounds.top - defaultBoundsWithShelf.top); + } + + @Test + public void getDefaultBounds_imeAndShelfShowing_offsetByTallest() { + final int imeHeight = 30; + final int shelfHeight = 40; + mPipBoundsState.setImeVisibility(false, 0); + mPipBoundsState.setShelfVisibility(false, 0); + final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds(); + + mPipBoundsState.setImeVisibility(true, imeHeight); + mPipBoundsState.setShelfVisibility(true, shelfHeight); + final Rect defaultBoundsWithIme = mPipBoundsAlgorithm.getDefaultBounds(); + + assertEquals(shelfHeight, defaultBounds.top - defaultBoundsWithIme.top); + } + + @Test + public void getDefaultBounds_boundsAtDefaultGravity() { + final Rect insetBounds = new Rect(); + mPipBoundsAlgorithm.getInsetBounds(insetBounds); + overrideDefaultStackGravity(Gravity.END | Gravity.BOTTOM); + + final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds(); + + assertEquals(insetBounds.bottom, defaultBounds.bottom); + assertEquals(insetBounds.right, defaultBounds.right); + } + + @Test + public void getNormalBounds_invalidAspectRatio_returnsDefaultBounds() { + final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds(); + + // Set an invalid current aspect ratio. + mPipBoundsState.setAspectRatio(MIN_ASPECT_RATIO / 2); + final Rect normalBounds = mPipBoundsAlgorithm.getNormalBounds(); + + assertEquals(defaultBounds, normalBounds); + } + + @Test + public void getNormalBounds_validAspectRatio_returnsAdjustedDefaultBounds() { + final Rect defaultBoundsAdjustedToAspectRatio = mPipBoundsAlgorithm.getDefaultBounds(); + mPipBoundsAlgorithm.transformBoundsToAspectRatio(defaultBoundsAdjustedToAspectRatio, + MIN_ASPECT_RATIO, false /* useCurrentMinEdgeSize */, false /* useCurrentSize */); + + // Set a valid current aspect ratio different that the default. + mPipBoundsState.setAspectRatio(MIN_ASPECT_RATIO); + final Rect normalBounds = mPipBoundsAlgorithm.getNormalBounds(); + + assertEquals(defaultBoundsAdjustedToAspectRatio, normalBounds); + } + + @Test + public void getEntryDestinationBounds_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 = mPipBoundsAlgorithm.getEntryDestinationBounds(); + final float actualAspectRatio = getRectAspectRatio(destinationBounds); + assertEquals("Destination bounds matches the given aspect ratio", + aspectRatio, actualAspectRatio, ASPECT_RATIO_ERROR_MARGIN); + } + } + + @Test + public void getEntryDestinationBounds_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 = mPipBoundsAlgorithm.getEntryDestinationBounds(); + final float actualAspectRatio = + destinationBounds.width() / (destinationBounds.height() * 1f); + assertEquals("Destination bounds fallbacks to default aspect ratio", + mPipBoundsAlgorithm.getDefaultAspectRatio(), actualAspectRatio, + ASPECT_RATIO_ERROR_MARGIN); + } + } + + @Test + public void getAdjustedDestinationBounds_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 = mPipBoundsAlgorithm.getAdjustedDestinationBounds( + currentBounds, aspectRatio); + + 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 getEntryDestinationBounds_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); + mPipBoundsState.setOverrideMinSize(minimalSize); + final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); + 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 getAdjustedDestinationBounds_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); + mPipBoundsState.setOverrideMinSize(minSize); + final Rect destinationBounds = mPipBoundsAlgorithm.getAdjustedDestinationBounds( + currentBounds, aspectRatio); + + assertTrue("Destination bounds ignores minimal size", + destinationBounds.width() > minSize.getWidth() + && destinationBounds.height() > minSize.getHeight()); + } + + @Test + public void getEntryDestinationBounds_reentryStateExists_restoreLastSize() { + mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO); + final Rect reentryBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); + reentryBounds.scale(1.25f); + final float reentrySnapFraction = mPipBoundsAlgorithm.getSnapFraction(reentryBounds); + + mPipBoundsState.saveReentryState( + new Size(reentryBounds.width(), reentryBounds.height()), reentrySnapFraction); + final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); + + assertEquals(reentryBounds.width(), destinationBounds.width()); + assertEquals(reentryBounds.height(), destinationBounds.height()); + } + + @Test + public void getEntryDestinationBounds_reentryStateExists_restoreLastPosition() { + mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO); + final Rect reentryBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); + reentryBounds.offset(0, -100); + final float reentrySnapFraction = mPipBoundsAlgorithm.getSnapFraction(reentryBounds); + + mPipBoundsState.saveReentryState( + new Size(reentryBounds.width(), reentryBounds.height()), reentrySnapFraction); + + final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); + + assertBoundsInclusionWithMargin("restoreLastPosition", reentryBounds, destinationBounds); + } + + @Test + public void setShelfHeight_offsetBounds() { + final int shelfHeight = 100; + mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO); + final Rect oldPosition = mPipBoundsAlgorithm.getEntryDestinationBounds(); + + mPipBoundsState.setShelfVisibility(true, shelfHeight); + final Rect newPosition = mPipBoundsAlgorithm.getEntryDestinationBounds(); + + 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 = mPipBoundsAlgorithm.getEntryDestinationBounds(); + + mPipBoundsState.setImeVisibility(true, imeHeight); + final Rect newPosition = mPipBoundsAlgorithm.getEntryDestinationBounds(); + + oldPosition.offset(0, -imeHeight); + assertBoundsInclusionWithMargin("offsetBounds by IME", oldPosition, newPosition); + } + + @Test + public void getEntryDestinationBounds_noReentryState_useDefaultBounds() { + mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO); + final Rect defaultBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); + + mPipBoundsState.clearReentryState(); + + final Rect actualBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); + + assertBoundsInclusionWithMargin("useDefaultBounds", defaultBounds, actualBounds); + } + + private void overrideDefaultAspectRatio(float aspectRatio) { + final TestableResources res = mContext.getOrCreateTestableResources(); + res.addOverride( + com.android.internal.R.dimen.config_pictureInPictureDefaultAspectRatio, + aspectRatio); + mPipBoundsAlgorithm.onConfigurationChanged(mContext); + } + + private void overrideDefaultStackGravity(int stackGravity) { + final TestableResources res = mContext.getOrCreateTestableResources(); + res.addOverride( + com.android.internal.R.integer.config_defaultPictureInPictureGravity, + stackGravity); + mPipBoundsAlgorithm.onConfigurationChanged(mContext); + } + + 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 static float getRectAspectRatio(Rect rect) { + return rect.width() / (rect.height() * 1f); + } +} 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..dea24d3c2ec0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.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.pip; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +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 com.android.internal.util.function.TriConsumer; +import com.android.wm.shell.ShellTestCase; + +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 ShellTestCase { + + private static final Size DEFAULT_SIZE = new Size(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(mContext); + 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 Size size = new Size(100, 100); + final float snapFraction = 0.5f; + + mPipBoundsState.saveReentryState(size, snapFraction); + + final PipBoundsState.PipReentryState state = mPipBoundsState.getReentryState(); + assertEquals(size, state.getSize()); + assertEquals(snapFraction, state.getSnapFraction(), 0.01); + } + + @Test + public void testClearReentryState() { + final Size size = new Size(100, 100); + final float snapFraction = 0.5f; + + mPipBoundsState.saveReentryState(size, snapFraction); + mPipBoundsState.clearReentryState(); + + assertNull(mPipBoundsState.getReentryState()); + } + + @Test + public void testSetLastPipComponentName_notChanged_doesNotClearReentryState() { + mPipBoundsState.setLastPipComponentName(mTestComponentName1); + mPipBoundsState.saveReentryState(DEFAULT_SIZE, DEFAULT_SNAP_FRACTION); + + mPipBoundsState.setLastPipComponentName(mTestComponentName1); + + final PipBoundsState.PipReentryState state = mPipBoundsState.getReentryState(); + assertNotNull(state); + assertEquals(DEFAULT_SIZE, state.getSize()); + assertEquals(DEFAULT_SNAP_FRACTION, state.getSnapFraction(), 0.01); + } + + @Test + public void testSetLastPipComponentName_changed_clearReentryState() { + mPipBoundsState.setLastPipComponentName(mTestComponentName1); + mPipBoundsState.saveReentryState(DEFAULT_SIZE, DEFAULT_SNAP_FRACTION); + + mPipBoundsState.setLastPipComponentName(mTestComponentName2); + + assertNull(mPipBoundsState.getReentryState()); + } + + @Test + public void testSetShelfVisibility_changed_callbackInvoked() { + final TriConsumer<Boolean, Integer, Boolean> callback = mock(TriConsumer.class); + mPipBoundsState.setOnShelfVisibilityChangeCallback(callback); + + mPipBoundsState.setShelfVisibility(true, 100); + + verify(callback).accept(true, 100, true); + } + + @Test + public void testSetShelfVisibility_changedWithoutUpdateMovBounds_callbackInvoked() { + final TriConsumer<Boolean, Integer, Boolean> callback = mock(TriConsumer.class); + mPipBoundsState.setOnShelfVisibilityChangeCallback(callback); + + mPipBoundsState.setShelfVisibility(true, 100, false); + + verify(callback).accept(true, 100, false); + } + + @Test + public void testSetShelfVisibility_notChanged_callbackNotInvoked() { + final TriConsumer<Boolean, Integer, Boolean> callback = mock(TriConsumer.class); + mPipBoundsState.setShelfVisibility(true, 100); + mPipBoundsState.setOnShelfVisibilityChangeCallback(callback); + + mPipBoundsState.setShelfVisibility(true, 100); + + verify(callback, never()).accept(true, 100, true); + } + + @Test + public void testSetOverrideMinSize_changed_callbackInvoked() { + final Runnable callback = mock(Runnable.class); + mPipBoundsState.setOverrideMinSize(new Size(5, 5)); + mPipBoundsState.setOnMinimalSizeChangeCallback(callback); + + mPipBoundsState.setOverrideMinSize(new Size(10, 10)); + + verify(callback).run(); + } + + @Test + public void testSetOverrideMinSize_notChanged_callbackNotInvoked() { + final Runnable callback = mock(Runnable.class); + mPipBoundsState.setOverrideMinSize(new Size(5, 5)); + mPipBoundsState.setOnMinimalSizeChangeCallback(callback); + + mPipBoundsState.setOverrideMinSize(new Size(5, 5)); + + verify(callback, never()).run(); + } + + @Test + public void testGetOverrideMinEdgeSize() { + mPipBoundsState.setOverrideMinSize(null); + assertEquals(0, mPipBoundsState.getOverrideMinEdgeSize()); + + mPipBoundsState.setOverrideMinSize(new Size(5, 10)); + assertEquals(5, mPipBoundsState.getOverrideMinEdgeSize()); + + mPipBoundsState.setOverrideMinSize(new Size(15, 10)); + assertEquals(10, mPipBoundsState.getOverrideMinEdgeSize()); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipSnapAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipSnapAlgorithmTest.java new file mode 100644 index 000000000000..dcee2e1847b2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipSnapAlgorithmTest.java @@ -0,0 +1,237 @@ +/* + * 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 android.graphics.Rect; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link PipSnapAlgorithm}. **/ +@RunWith(AndroidTestingRunner.class) +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class PipSnapAlgorithmTest extends ShellTestCase { + private static final int DEFAULT_STASH_OFFSET = 32; + private static final Rect DISPLAY_BOUNDS = new Rect(0, 0, 2000, 2000); + private static final Rect STACK_BOUNDS_CENTERED = new Rect(900, 900, 1100, 1100); + private static final Rect MOVEMENT_BOUNDS = new Rect(0, 0, + DISPLAY_BOUNDS.width() - STACK_BOUNDS_CENTERED.width(), + DISPLAY_BOUNDS.width() - STACK_BOUNDS_CENTERED.width()); + + private PipSnapAlgorithm mPipSnapAlgorithm; + + @Before + public void setUp() { + mPipSnapAlgorithm = new PipSnapAlgorithm(); + } + + @Test + public void testApplySnapFraction_topEdge() { + final float snapFraction = 0.25f; + final Rect bounds = new Rect(STACK_BOUNDS_CENTERED); + + mPipSnapAlgorithm.applySnapFraction(bounds, MOVEMENT_BOUNDS, snapFraction); + + assertEquals(MOVEMENT_BOUNDS.width() / 4, bounds.left); + assertEquals(MOVEMENT_BOUNDS.top, bounds.top); + } + + @Test + public void testApplySnapFraction_rightEdge() { + final float snapFraction = 1.5f; + final Rect bounds = new Rect(STACK_BOUNDS_CENTERED); + + mPipSnapAlgorithm.applySnapFraction(bounds, MOVEMENT_BOUNDS, snapFraction); + + assertEquals(MOVEMENT_BOUNDS.right, bounds.left); + assertEquals(MOVEMENT_BOUNDS.height() / 2, bounds.top); + } + + @Test + public void testApplySnapFraction_bottomEdge() { + final float snapFraction = 2.25f; + final Rect bounds = new Rect(STACK_BOUNDS_CENTERED); + + mPipSnapAlgorithm.applySnapFraction(bounds, MOVEMENT_BOUNDS, snapFraction); + + assertEquals((int) (MOVEMENT_BOUNDS.width() * 0.75f), bounds.left); + assertEquals(MOVEMENT_BOUNDS.bottom, bounds.top); + } + + @Test + public void testApplySnapFraction_leftEdge() { + final float snapFraction = 3.75f; + final Rect bounds = new Rect(STACK_BOUNDS_CENTERED); + + mPipSnapAlgorithm.applySnapFraction(bounds, MOVEMENT_BOUNDS, snapFraction); + + assertEquals(MOVEMENT_BOUNDS.left, bounds.left); + assertEquals((int) (MOVEMENT_BOUNDS.height() * 0.25f), bounds.top); + } + + @Test + public void testApplySnapFraction_notStashed_isNotOffBounds() { + final float snapFraction = 2f; + final Rect bounds = new Rect(STACK_BOUNDS_CENTERED); + + mPipSnapAlgorithm.applySnapFraction(bounds, MOVEMENT_BOUNDS, snapFraction, + PipBoundsState.STASH_TYPE_NONE, DEFAULT_STASH_OFFSET, DISPLAY_BOUNDS); + + assertEquals(MOVEMENT_BOUNDS.right, bounds.left); + assertEquals(MOVEMENT_BOUNDS.bottom, bounds.top); + } + + @Test + public void testApplySnapFraction_stashedLeft() { + final float snapFraction = 3f; + final Rect bounds = new Rect(STACK_BOUNDS_CENTERED); + + mPipSnapAlgorithm.applySnapFraction(bounds, MOVEMENT_BOUNDS, snapFraction, + PipBoundsState.STASH_TYPE_LEFT, DEFAULT_STASH_OFFSET, DISPLAY_BOUNDS); + + final int offBoundsWidth = bounds.width() - DEFAULT_STASH_OFFSET; + assertEquals(MOVEMENT_BOUNDS.left - offBoundsWidth, bounds.left); + assertEquals(MOVEMENT_BOUNDS.bottom, bounds.top); + } + + @Test + public void testApplySnapFraction_stashedRight() { + final float snapFraction = 2f; + final Rect bounds = new Rect(STACK_BOUNDS_CENTERED); + + mPipSnapAlgorithm.applySnapFraction(bounds, MOVEMENT_BOUNDS, snapFraction, + PipBoundsState.STASH_TYPE_RIGHT, DEFAULT_STASH_OFFSET, DISPLAY_BOUNDS); + + assertEquals(DISPLAY_BOUNDS.right - DEFAULT_STASH_OFFSET, bounds.left); + assertEquals(MOVEMENT_BOUNDS.bottom, bounds.top); + } + + @Test + public void testSnapRectToClosestEdge_rightEdge() { + final Rect bounds = new Rect(STACK_BOUNDS_CENTERED); + // Move the centered rect slightly to the right side. + bounds.offset(10, 0); + + mPipSnapAlgorithm.snapRectToClosestEdge(bounds, MOVEMENT_BOUNDS, bounds, + PipBoundsState.STASH_TYPE_NONE); + + assertEquals(MOVEMENT_BOUNDS.right, bounds.left); + } + + @Test + public void testSnapRectToClosestEdge_leftEdge() { + final Rect bounds = new Rect(STACK_BOUNDS_CENTERED); + // Move the centered rect slightly to the left side. + bounds.offset(-10, 0); + + mPipSnapAlgorithm.snapRectToClosestEdge(bounds, MOVEMENT_BOUNDS, bounds, + PipBoundsState.STASH_TYPE_NONE); + + assertEquals(MOVEMENT_BOUNDS.left, bounds.left); + } + + @Test + public void testSnapRectToClosestEdge_topEdge() { + final Rect bounds = new Rect(STACK_BOUNDS_CENTERED); + // Move the centered rect slightly to the top half. + bounds.offset(0, -10); + + mPipSnapAlgorithm.snapRectToClosestEdge(bounds, MOVEMENT_BOUNDS, bounds, + PipBoundsState.STASH_TYPE_NONE); + + assertEquals(MOVEMENT_BOUNDS.top, bounds.top); + } + + @Test + public void testSnapRectToClosestEdge_bottomEdge() { + final Rect bounds = new Rect(STACK_BOUNDS_CENTERED); + // Move the centered rect slightly to the bottom half. + bounds.offset(0, 10); + + mPipSnapAlgorithm.snapRectToClosestEdge(bounds, MOVEMENT_BOUNDS, bounds, + PipBoundsState.STASH_TYPE_NONE); + + assertEquals(MOVEMENT_BOUNDS.bottom, bounds.top); + } + + @Test + public void testSnapRectToClosestEdge_stashed_unStahesBounds() { + final Rect bounds = new Rect(STACK_BOUNDS_CENTERED); + // Stash it on the left side. + mPipSnapAlgorithm.applySnapFraction(bounds, MOVEMENT_BOUNDS, 3.5f, + PipBoundsState.STASH_TYPE_LEFT, DEFAULT_STASH_OFFSET, DISPLAY_BOUNDS); + + mPipSnapAlgorithm.snapRectToClosestEdge(bounds, MOVEMENT_BOUNDS, bounds, + PipBoundsState.STASH_TYPE_LEFT); + + assertEquals(MOVEMENT_BOUNDS.left, bounds.left); + } + + @Test + public void testGetSnapFraction_leftEdge() { + final Rect bounds = new Rect(STACK_BOUNDS_CENTERED); + // Move it slightly to the left side. + bounds.offset(-10, 0); + + final float snapFraction = mPipSnapAlgorithm.getSnapFraction(bounds, MOVEMENT_BOUNDS); + + assertEquals(3.5f, snapFraction, 0.1f); + } + + @Test + public void testGetSnapFraction_rightEdge() { + final Rect bounds = new Rect(STACK_BOUNDS_CENTERED); + // Move it slightly to the right side. + bounds.offset(10, 0); + + final float snapFraction = mPipSnapAlgorithm.getSnapFraction(bounds, MOVEMENT_BOUNDS); + + assertEquals(1.5f, snapFraction, 0.1f); + } + + @Test + public void testGetSnapFraction_topEdge() { + final Rect bounds = new Rect(STACK_BOUNDS_CENTERED); + // Move it slightly to the top half. + bounds.offset(0, -10); + + final float snapFraction = mPipSnapAlgorithm.getSnapFraction(bounds, MOVEMENT_BOUNDS); + + assertEquals(0.5f, snapFraction, 0.1f); + } + + @Test + public void testGetSnapFraction_bottomEdge() { + final Rect bounds = new Rect(STACK_BOUNDS_CENTERED); + // Move it slightly to the bottom half. + bounds.offset(0, 10); + + final float snapFraction = mPipSnapAlgorithm.getSnapFraction(bounds, MOVEMENT_BOUNDS); + + assertEquals(2.5f, snapFraction, 0.1f); + } +} 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..7a810a1742d7 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java @@ -0,0 +1,240 @@ +/* + * 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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doNothing; +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.app.ActivityManager; +import android.app.PictureInPictureParams; +import android.content.ComponentName; +import android.content.pm.ActivityInfo; +import android.graphics.Rect; +import android.os.RemoteException; +import android.test.suitebuilder.annotation.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.util.Rational; +import android.util.Size; +import android.view.Display; +import android.view.DisplayInfo; +import android.window.WindowContainerToken; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.pip.phone.PhonePipMenuController; +import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; + +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 ShellTestCase { + private PipTaskOrganizer mSpiedPipTaskOrganizer; + + @Mock private DisplayController mMockdDisplayController; + @Mock private PipBoundsAlgorithm mMockPipBoundsAlgorithm; + @Mock private PhonePipMenuController mMockPhonePipMenuController; + @Mock private PipSurfaceTransactionHelper mMockPipSurfaceTransactionHelper; + @Mock private PipUiEventLogger mMockPipUiEventLogger; + @Mock private Optional<LegacySplitScreen> mMockOptionalSplitScreen; + @Mock private ShellTaskOrganizer mMockShellTaskOrganizer; + private TestShellExecutor mMainExecutor; + private PipBoundsState mPipBoundsState; + + private ComponentName mComponent1; + private ComponentName mComponent2; + + @Before + public void setUp() throws RemoteException { + MockitoAnnotations.initMocks(this); + mComponent1 = new ComponentName(mContext, "component1"); + mComponent2 = new ComponentName(mContext, "component2"); + mPipBoundsState = new PipBoundsState(mContext); + mMainExecutor = new TestShellExecutor(); + mSpiedPipTaskOrganizer = spy(new PipTaskOrganizer(mContext, mPipBoundsState, + mMockPipBoundsAlgorithm, mMockPhonePipMenuController, + mMockPipSurfaceTransactionHelper, mMockOptionalSplitScreen, mMockdDisplayController, + mMockPipUiEventLogger, mMockShellTaskOrganizer, mMainExecutor)); + mMainExecutor.flushAll(); + preparePipTaskOrg(); + } + + @Test + public void instantiatePipTaskOrganizer_addsTaskListener() { + verify(mMockShellTaskOrganizer).addListenerForType(any(), anyInt()); + } + + @Test + public void instantiatePipTaskOrganizer_addsDisplayWindowListener() { + verify(mMockdDisplayController).addDisplayWindowListener(any()); + } + + @Test + public void startSwipePipToHome_updatesAspectRatio() { + final Rational aspectRatio = new Rational(2, 1); + + mSpiedPipTaskOrganizer.startSwipePipToHome(mComponent1, null, createPipParams(aspectRatio)); + + assertEquals(aspectRatio.floatValue(), mPipBoundsState.getAspectRatio(), 0.01f); + } + + @Test + public void startSwipePipToHome_updatesLastPipComponentName() { + mSpiedPipTaskOrganizer.startSwipePipToHome(mComponent1, null, null); + + assertEquals(mComponent1, mPipBoundsState.getLastPipComponentName()); + } + + @Test + public void startSwipePipToHome_updatesOverrideMinSize() { + final Size minSize = new Size(100, 80); + + mSpiedPipTaskOrganizer.startSwipePipToHome(mComponent1, createActivityInfo(minSize), null); + + assertEquals(minSize, mPipBoundsState.getOverrideMinSize()); + } + + @Test + public void onTaskAppeared_updatesAspectRatio() { + final Rational aspectRatio = new Rational(2, 1); + + mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, + createPipParams(aspectRatio)), null /* leash */); + + assertEquals(aspectRatio.floatValue(), mPipBoundsState.getAspectRatio(), 0.01f); + } + + @Test + public void onTaskAppeared_updatesLastPipComponentName() { + mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, createPipParams(null)), + null /* leash */); + + assertEquals(mComponent1, mPipBoundsState.getLastPipComponentName()); + } + + @Test + public void onTaskAppeared_updatesOverrideMinSize() { + final Size minSize = new Size(100, 80); + + mSpiedPipTaskOrganizer.onTaskAppeared( + createTaskInfo(mComponent1, createPipParams(null), minSize), + null /* leash */); + + assertEquals(minSize, mPipBoundsState.getOverrideMinSize()); + } + + @Test + public void onTaskInfoChanged_updatesAspectRatioIfChanged() { + final Rational startAspectRatio = new Rational(2, 1); + final Rational newAspectRatio = new Rational(1, 2); + mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, + createPipParams(startAspectRatio)), null /* leash */); + + mSpiedPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent1, + createPipParams(newAspectRatio))); + + assertEquals(newAspectRatio.floatValue(), mPipBoundsState.getAspectRatio(), 0.01f); + } + + @Test + public void onTaskInfoChanged_updatesLastPipComponentName() { + mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, + createPipParams(null)), null /* leash */); + + mSpiedPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent2, + createPipParams(null))); + + assertEquals(mComponent2, mPipBoundsState.getLastPipComponentName()); + } + + @Test + public void onTaskInfoChanged_updatesOverrideMinSize() { + mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, + createPipParams(null)), null /* leash */); + + final Size minSize = new Size(100, 80); + mSpiedPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent2, + createPipParams(null), minSize)); + + assertEquals(minSize, mPipBoundsState.getOverrideMinSize()); + } + + private void preparePipTaskOrg() { + final DisplayInfo info = new DisplayInfo(); + mPipBoundsState.setDisplayLayout(new DisplayLayout(info, + mContext.getResources(), true, true)); + when(mMockPipBoundsAlgorithm.getEntryDestinationBounds()).thenReturn(new Rect()); + when(mMockPipBoundsAlgorithm.getAdjustedDestinationBounds(any(), anyFloat())) + .thenReturn(new Rect()); + mSpiedPipTaskOrganizer.setOneShotAnimationType(PipAnimationController.ANIM_TYPE_ALPHA); + doNothing().when(mSpiedPipTaskOrganizer).enterPipWithAlphaAnimation(any(), anyLong()); + doNothing().when(mSpiedPipTaskOrganizer).scheduleAnimateResizePip(any(), anyInt(), any()); + } + + private static ActivityManager.RunningTaskInfo createTaskInfo( + ComponentName componentName, PictureInPictureParams params) { + return createTaskInfo(componentName, params, null /* minSize */); + } + + private static ActivityManager.RunningTaskInfo createTaskInfo( + ComponentName componentName, PictureInPictureParams params, Size minSize) { + final ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo(); + info.token = mock(WindowContainerToken.class); + info.pictureInPictureParams = params; + info.topActivity = componentName; + if (minSize != null) { + info.topActivityInfo = createActivityInfo(minSize); + } + return info; + } + + private static ActivityInfo createActivityInfo(Size minSize) { + final ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.windowLayout = new ActivityInfo.WindowLayout( + 0, 0, 0, 0, 0, minSize.getWidth(), minSize.getHeight()); + return activityInfo; + } + + private static PictureInPictureParams createPipParams(Rational aspectRatio) { + return new PictureInPictureParams.Builder() + .setAspectRatio(aspectRatio) + .build(); + } +} 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..62ffac4fbd3f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -0,0 +1,164 @@ +/* + * 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.doAnswer; +import static org.mockito.Mockito.mock; +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.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Rect; +import android.os.RemoteException; +import android.test.suitebuilder.annotation.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.util.Size; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.WindowManagerShellWrapper; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TaskStackListenerImpl; +import com.android.wm.shell.pip.PipBoundsAlgorithm; +import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipMediaController; +import com.android.wm.shell.pip.PipTaskOrganizer; + +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 ShellTestCase { + private PipController mPipController; + + @Mock private DisplayController mMockDisplayController; + @Mock private PhonePipMenuController mMockPhonePipMenuController; + @Mock private PipAppOpsListener mMockPipAppOpsListener; + @Mock private PipBoundsAlgorithm mMockPipBoundsAlgorithm; + @Mock private PipMediaController mMockPipMediaController; + @Mock private PipTaskOrganizer mMockPipTaskOrganizer; + @Mock private PipTouchHandler mMockPipTouchHandler; + @Mock private WindowManagerShellWrapper mMockWindowManagerShellWrapper; + @Mock private PipBoundsState mMockPipBoundsState; + @Mock private TaskStackListenerImpl mMockTaskStackListener; + @Mock private ShellExecutor mMockExecutor; + + @Before + public void setUp() throws RemoteException { + MockitoAnnotations.initMocks(this); + mPipController = new PipController(mContext, mMockDisplayController, + mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipBoundsState, + mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer, + mMockPipTouchHandler, mMockWindowManagerShellWrapper, mMockTaskStackListener, + mMockExecutor); + doAnswer(invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }).when(mMockExecutor).execute(any()); + } + + @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, mMockPipBoundsAlgorithm, mMockPipBoundsState, + mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer, + mMockPipTouchHandler, mMockWindowManagerShellWrapper, mMockTaskStackListener, + mMockExecutor)); + } + + @Test + public void onActivityHidden_isLastPipComponentName_clearLastPipComponent() { + final ComponentName component1 = new ComponentName(mContext, "component1"); + when(mMockPipBoundsState.getLastPipComponentName()).thenReturn(component1); + + mPipController.mPinnedStackListener.onActivityHidden(component1); + + verify(mMockPipBoundsState).setLastPipComponentName(null); + } + + @Test + public void onActivityHidden_isNotLastPipComponentName_lastPipComponentNotCleared() { + final ComponentName component1 = new ComponentName(mContext, "component1"); + final ComponentName component2 = new ComponentName(mContext, "component2"); + when(mMockPipBoundsState.getLastPipComponentName()).thenReturn(component1); + + mPipController.mPinnedStackListener.onActivityHidden(component2); + + verify(mMockPipBoundsState, never()).setLastPipComponentName(null); + } + + @Test + public void saveReentryState_noUserResize_doesNotSaveSize() { + final Rect bounds = new Rect(0, 0, 10, 10); + when(mMockPipBoundsAlgorithm.getSnapFraction(bounds)).thenReturn(1.0f); + when(mMockPipBoundsState.hasUserResizedPip()).thenReturn(false); + + mPipController.saveReentryState(bounds); + + verify(mMockPipBoundsState).saveReentryState(null, 1.0f); + } + + @Test + public void saveReentryState_userHasResized_savesSize() { + final Rect bounds = new Rect(0, 0, 10, 10); + final Rect resizedBounds = new Rect(0, 0, 30, 30); + when(mMockPipBoundsAlgorithm.getSnapFraction(bounds)).thenReturn(1.0f); + when(mMockPipTouchHandler.getUserResizeBounds()).thenReturn(resizedBounds); + when(mMockPipBoundsState.hasUserResizedPip()).thenReturn(true); + + mPipController.saveReentryState(bounds); + + verify(mMockPipBoundsState).saveReentryState(new Size(30, 30), 1.0f); + } +} 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..b4cfbc281d61 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java @@ -0,0 +1,163 @@ +/* + * 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.ShellTestCase; +import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.pip.PipBoundsAlgorithm; +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.PipUiEventLogger; + +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 ShellTestCase { + + private PipTouchHandler mPipTouchHandler; + + @Mock + private PhonePipMenuController mPhonePipMenuController; + + @Mock + private PipTaskOrganizer mPipTaskOrganizer; + + @Mock + private FloatingContentCoordinator mFloatingContentCoordinator; + + @Mock + private PipUiEventLogger mPipUiEventLogger; + + @Mock + private ShellExecutor mMainExecutor; + + private PipBoundsState mPipBoundsState; + private PipBoundsAlgorithm mPipBoundsAlgorithm; + 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(mContext); + mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState); + mPipSnapAlgorithm = mPipBoundsAlgorithm.getSnapAlgorithm(); + mPipSnapAlgorithm = new PipSnapAlgorithm(); + mPipTouchHandler = new PipTouchHandler(mContext, mPhonePipMenuController, + mPipBoundsAlgorithm, mPipBoundsState, mPipTaskOrganizer, + mFloatingContentCoordinator, mPipUiEventLogger, mMainExecutor); + 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(); + mPipBoundsAlgorithm.getMovementBounds(mMinBounds, mInsetBounds, expectedMinMovementBounds, + 0); + + mPipTouchHandler.onMovementBoundsChanged(mInsetBounds, mMinBounds, mCurBounds, + mFromImeAdjustment, mFromShelfAdjustment, mDisplayRotation); + + assertEquals(expectedMinMovementBounds, mPipBoundsState.getNormalMovementBounds()); + verify(mPipResizeGestureHandler, times(1)) + .updateMinSize(mMinBounds.width(), mMinBounds.height()); + } + + @Test + public void updateMovementBounds_maxBounds() { + Point displaySize = new Point(); + mContext.getDisplay().getRealSize(displaySize); + Size maxSize = mPipBoundsAlgorithm.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(); + mPipBoundsAlgorithm.getMovementBounds(maxBounds, mInsetBounds, expectedMaxMovementBounds, + 0); + + mPipTouchHandler.onMovementBoundsChanged(mInsetBounds, mMinBounds, mCurBounds, + mFromImeAdjustment, mFromShelfAdjustment, mDisplayRotation); + + assertEquals(expectedMaxMovementBounds, mPipBoundsState.getExpandedMovementBounds()); + 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..35656bde7169 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchStateTest.java @@ -0,0 +1,148 @@ +/* + * 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.SystemClock; +import android.testing.AndroidTestingRunner; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.CountDownLatch; + +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class PipTouchStateTest extends ShellTestCase { + + private PipTouchState mTouchState; + private CountDownLatch mDoubleTapCallbackTriggeredLatch; + private CountDownLatch mHoverExitCallbackTriggeredLatch; + private TestShellExecutor mMainExecutor; + + @Before + public void setUp() throws Exception { + mMainExecutor = new TestShellExecutor(); + mDoubleTapCallbackTriggeredLatch = new CountDownLatch(1); + mHoverExitCallbackTriggeredLatch = new CountDownLatch(1); + mTouchState = new PipTouchState(ViewConfiguration.get(getContext()), + mDoubleTapCallbackTriggeredLatch::countDown, + mHoverExitCallbackTriggeredLatch::countDown, + mMainExecutor); + 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(); + + mMainExecutor.flushAll(); + 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(); + mMainExecutor.flushAll(); + assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 0); + } + + @Test + public void testHoverExitTimeout_timeoutCallbackNotCalled() throws Exception { + mTouchState.scheduleHoverExitTimeoutCallback(); + assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 1); + } + + @Test + public void testHoverExitTimeout_timeoutCallbackNotCalled_ifButtonPress() throws Exception { + mTouchState.scheduleHoverExitTimeoutCallback(); + mTouchState.onTouchEvent(createMotionEvent(ACTION_BUTTON_PRESS, SystemClock.uptimeMillis(), + 0, 0)); + mMainExecutor.flushAll(); + 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/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUIControllerTest.java new file mode 100644 index 000000000000..98f01ff08deb --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUIControllerTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.sizecompatui; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.graphics.Rect; +import android.os.IBinder; +import android.testing.AndroidTestingRunner; +import android.view.View; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayImeController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link SizeCompatUIController}. + * + * Build/Install/Run: + * atest WMShellUnitTests:SizeCompatUIControllerTest + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class SizeCompatUIControllerTest extends ShellTestCase { + private static final int DISPLAY_ID = 0; + + private final TestShellExecutor mShellMainExecutor = new TestShellExecutor(); + + private SizeCompatUIController mController; + private @Mock DisplayController mMockDisplayController; + private @Mock DisplayImeController mMockImeController; + private @Mock SizeCompatRestartButton mMockButton; + private @Mock IBinder mMockActivityToken; + private @Mock ShellTaskOrganizer.TaskListener mMockTaskListener; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + doReturn(true).when(mMockButton).show(); + + mController = new SizeCompatUIController(mContext, mMockDisplayController, + mMockImeController, mShellMainExecutor) { + @Override + SizeCompatRestartButton createRestartButton(Context context, int displayId) { + return mMockButton; + } + }; + } + + @Test + public void testListenerRegistered() { + verify(mMockDisplayController).addDisplayWindowListener(mController); + verify(mMockImeController).addPositionProcessor(mController); + } + + @Test + public void testOnSizeCompatInfoChanged() { + final int taskId = 12; + final Rect taskBounds = new Rect(0, 0, 1000, 2000); + + // Verify that the restart button is added with non-null size compat activity. + mController.mImpl.onSizeCompatInfoChanged(DISPLAY_ID, taskId, taskBounds, + mMockActivityToken, mMockTaskListener); + mShellMainExecutor.flushAll(); + + verify(mMockButton).show(); + verify(mMockButton).updateLastTargetActivity(eq(mMockActivityToken)); + + // Verify that the restart button is removed with null size compat activity. + mController.mImpl.onSizeCompatInfoChanged(DISPLAY_ID, taskId, null, null, null); + + mShellMainExecutor.flushAll(); + verify(mMockButton).remove(); + } + + @Test + public void testChangeButtonVisibilityOnImeShowHide() { + final int taskId = 12; + final Rect taskBounds = new Rect(0, 0, 1000, 2000); + mController.mImpl.onSizeCompatInfoChanged(DISPLAY_ID, taskId, taskBounds, + mMockActivityToken, mMockTaskListener); + mShellMainExecutor.flushAll(); + + // Verify that the restart button is hidden when IME is visible. + doReturn(View.VISIBLE).when(mMockButton).getVisibility(); + mController.onImeVisibilityChanged(DISPLAY_ID, true /* isShowing */); + + verify(mMockButton).setVisibility(eq(View.GONE)); + + // Verify that the restart button is visible when IME is hidden. + doReturn(View.GONE).when(mMockButton).getVisibility(); + mController.onImeVisibilityChanged(DISPLAY_ID, false /* isShowing */); + + verify(mMockButton).setVisibility(eq(View.VISIBLE)); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java new file mode 100644 index 000000000000..702e8945de01 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.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.splitscreen; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.ActivityManager; +import android.view.SurfaceControl; +import android.window.WindowContainerTransaction; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.TestRunningTaskInfoBuilder; +import com.android.wm.shell.common.SyncTransactionQueue; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +/** Tests for {@link MainStage} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class MainStageTests { + @Mock private ShellTaskOrganizer mTaskOrganizer; + @Mock private StageTaskListener.StageListenerCallbacks mCallbacks; + @Mock private SyncTransactionQueue mSyncQueue; + @Mock private ActivityManager.RunningTaskInfo mRootTaskInfo; + @Mock private SurfaceControl mRootLeash; + @Spy private WindowContainerTransaction mWct; + private MainStage mMainStage; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mRootTaskInfo = new TestRunningTaskInfoBuilder().build(); + mMainStage = new MainStage(mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, mSyncQueue); + mMainStage.onTaskAppeared(mRootTaskInfo, mRootLeash); + } + + @Test + public void testActiveDeactivate() { + mMainStage.activate(mRootTaskInfo.configuration.windowConfiguration.getBounds(), mWct); + assertThat(mMainStage.isActive()).isTrue(); + + mMainStage.deactivate(mWct); + assertThat(mMainStage.isActive()).isFalse(); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java new file mode 100644 index 000000000000..01888b758bf6 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java @@ -0,0 +1,83 @@ +/* + * 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.view.Display.DEFAULT_DISPLAY; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager; +import android.view.SurfaceControl; +import android.window.WindowContainerTransaction; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.TestRunningTaskInfoBuilder; +import com.android.wm.shell.common.SyncTransactionQueue; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +/** Tests for {@link SideStage} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SideStageTests { + @Mock private ShellTaskOrganizer mTaskOrganizer; + @Mock private StageTaskListener.StageListenerCallbacks mCallbacks; + @Mock private SyncTransactionQueue mSyncQueue; + @Mock private ActivityManager.RunningTaskInfo mRootTask; + @Mock private SurfaceControl mRootLeash; + @Spy private WindowContainerTransaction mWct; + private SideStage mSideStage; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mRootTask = new TestRunningTaskInfoBuilder().build(); + mSideStage = new SideStage(mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, mSyncQueue); + mSideStage.onTaskAppeared(mRootTask, mRootLeash); + } + + @Test + public void testAddTask() { + final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); + + mSideStage.addTask(task, mRootTask.configuration.windowConfiguration.getBounds(), mWct); + + verify(mWct).reparent(eq(task.token), eq(mRootTask.token), eq(true)); + } + + @Test + public void testRemoveTask() { + final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); + assertThat(mSideStage.removeTask(task.taskId, null, mWct)).isFalse(); + + mSideStage.mChildrenTaskInfo.put(task.taskId, task); + assertThat(mSideStage.removeTask(task.taskId, null, mWct)).isTrue(); + verify(mWct).reparent(eq(task.token), isNull(), eq(false)); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java new file mode 100644 index 000000000000..d2d18129d071 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -0,0 +1,109 @@ +/* + * 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.view.Display.DEFAULT_DISPLAY; +import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER; + +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_BOTTOM_OR_RIGHT; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager; +import android.content.Context; +import android.graphics.Rect; +import android.window.DisplayAreaInfo; +import android.window.IWindowContainerToken; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestRunningTaskInfoBuilder; +import com.android.wm.shell.common.SyncTransactionQueue; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Tests for {@link StageCoordinator} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class StageCoordinatorTests extends ShellTestCase { + @Mock private ShellTaskOrganizer mTaskOrganizer; + @Mock private SyncTransactionQueue mSyncQueue; + @Mock private RootTaskDisplayAreaOrganizer mRootTDAOrganizer; + @Mock private MainStage mMainStage; + @Mock private SideStage mSideStage; + private StageCoordinator mStageCoordinator; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mStageCoordinator = new TestStageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, + mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage); + } + + @Test + public void testMoveToSideStage() { + final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); + + mStageCoordinator.moveToSideStage(task, STAGE_POSITION_BOTTOM_OR_RIGHT); + + verify(mMainStage).activate(any(Rect.class), any(WindowContainerTransaction.class)); + verify(mSideStage).addTask(eq(task), any(Rect.class), + any(WindowContainerTransaction.class)); + } + + @Test + public void testRemoveFromSideStage() { + final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); + + doReturn(false).when(mMainStage).isActive(); + mStageCoordinator.removeFromSideStage(task.taskId); + + verify(mSideStage).removeTask( + eq(task.taskId), any(), any(WindowContainerTransaction.class)); + } + + private static class TestStageCoordinator extends StageCoordinator { + final DisplayAreaInfo mDisplayAreaInfo; + + TestStageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, + RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer, + MainStage mainStage, SideStage sideStage) { + super(context, displayId, syncQueue, rootTDAOrganizer, taskOrganizer, mainStage, + sideStage); + + // Prepare default TaskDisplayArea for testing. + mDisplayAreaInfo = new DisplayAreaInfo( + new WindowContainerToken(new IWindowContainerToken.Default()), + DEFAULT_DISPLAY, + FEATURE_DEFAULT_TASK_CONTAINER); + this.onDisplayAreaAppeared(mDisplayAreaInfo); + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java new file mode 100644 index 000000000000..c66e0730422c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.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.splitscreen; + +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager; +import android.view.SurfaceControl; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.TestRunningTaskInfoBuilder; +import com.android.wm.shell.common.SyncTransactionQueue; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Tests for {@link StageTaskListener} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class StageTaskListenerTests { + @Mock private ShellTaskOrganizer mTaskOrganizer; + @Mock private StageTaskListener.StageListenerCallbacks mCallbacks; + @Mock private SyncTransactionQueue mSyncQueue; + private ActivityManager.RunningTaskInfo mRootTask; + private StageTaskListener mStageTaskListener; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mStageTaskListener = new StageTaskListener( + mTaskOrganizer, + DEFAULT_DISPLAY, + mCallbacks, + mSyncQueue); + mRootTask = new TestRunningTaskInfoBuilder().build(); + mRootTask.parentTaskId = INVALID_TASK_ID; + mStageTaskListener.onTaskAppeared(mRootTask, new SurfaceControl()); + } + + @Test + public void testRootTaskAppeared() { + assertThat(mStageTaskListener.mRootTaskInfo.taskId).isEqualTo(mRootTask.taskId); + verify(mCallbacks).onRootTaskAppeared(); + verify(mCallbacks).onStatusChanged(eq(mRootTask.isVisible), eq(false)); + } + + @Test + public void testChildTaskAppeared() { + final ActivityManager.RunningTaskInfo childTask = + new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build(); + + mStageTaskListener.onTaskAppeared(childTask, new SurfaceControl()); + + assertThat(mStageTaskListener.mChildrenTaskInfo.contains(childTask.taskId)).isTrue(); + verify(mCallbacks).onStatusChanged(eq(mRootTask.isVisible), eq(true)); + } + + @Test(expected = IllegalArgumentException.class) + public void testUnknownTaskVanished() { + final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); + mStageTaskListener.onTaskVanished(task); + } + + @Test + public void testTaskVanished() { + final ActivityManager.RunningTaskInfo childTask = + new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build(); + mStageTaskListener.mRootTaskInfo = mRootTask; + mStageTaskListener.mChildrenTaskInfo.put(childTask.taskId, childTask); + + mStageTaskListener.onTaskVanished(childTask); + verify(mCallbacks, times(2)).onStatusChanged(eq(mRootTask.isVisible), eq(false)); + + mStageTaskListener.onTaskVanished(mRootTask); + verify(mCallbacks).onRootTaskVanished(); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java new file mode 100644 index 000000000000..c9537afa37ef --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java @@ -0,0 +1,165 @@ +/* + * 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 unittest.src.com.android.wm.shell.startingsurface; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.testing.TestableContext; +import android.view.View; +import android.view.WindowManager; +import android.view.WindowMetrics; +import android.window.StartingWindowInfo; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.wm.shell.common.HandlerExecutor; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.startingsurface.StartingSurfaceDrawer; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for the starting surface drawer. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class StartingSurfaceDrawerTests { + @Mock + private IBinder mBinder; + @Mock + private WindowManager mMockWindowManager; + + TestStartingSurfaceDrawer mStartingSurfaceDrawer; + + static final class TestStartingSurfaceDrawer extends StartingSurfaceDrawer{ + int mAddWindowForTask = 0; + int mViewThemeResId; + + TestStartingSurfaceDrawer(Context context, ShellExecutor executor) { + super(context, executor); + } + + @Override + protected void postAddWindow(int taskId, IBinder appToken, + View view, WindowManager wm, WindowManager.LayoutParams params) { + // listen for addView + mAddWindowForTask = taskId; + mViewThemeResId = view.getContext().getThemeResId(); + } + + @Override + protected void removeWindowSynced(int taskId) { + // listen for removeView + if (mAddWindowForTask == taskId) { + mAddWindowForTask = 0; + } + } + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + final TestableContext context = new TestableContext( + InstrumentationRegistry.getInstrumentation().getTargetContext(), null); + final WindowManager realWindowManager = context.getSystemService(WindowManager.class); + final WindowMetrics metrics = realWindowManager.getMaximumWindowMetrics(); + context.addMockSystemService(WindowManager.class, mMockWindowManager); + + spyOn(context); + spyOn(realWindowManager); + try { + doReturn(context).when(context).createPackageContext(anyString(), anyInt()); + } catch (PackageManager.NameNotFoundException e) { + // + } + doReturn(metrics).when(mMockWindowManager).getMaximumWindowMetrics(); + doNothing().when(mMockWindowManager).addView(any(), any()); + + mStartingSurfaceDrawer = spy(new TestStartingSurfaceDrawer(context, + new HandlerExecutor(new Handler(Looper.getMainLooper())))); + } + + @Test + public void testAddSplashScreenSurface() { + final int taskId = 1; + final Handler mainLoop = new Handler(Looper.getMainLooper()); + final StartingWindowInfo windowInfo = + createWindowInfo(taskId, android.R.style.Theme); + mStartingSurfaceDrawer.addStartingWindow(windowInfo, mBinder); + waitHandlerIdle(mainLoop); + verify(mStartingSurfaceDrawer).postAddWindow(eq(taskId), eq(mBinder), any(), any(), any()); + assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, taskId); + + mStartingSurfaceDrawer.removeStartingWindow(windowInfo.taskInfo.taskId); + waitHandlerIdle(mainLoop); + verify(mStartingSurfaceDrawer).removeWindowSynced(eq(taskId)); + assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, 0); + } + + @Test + public void testFallbackDefaultTheme() { + final int taskId = 1; + final Handler mainLoop = new Handler(Looper.getMainLooper()); + final StartingWindowInfo windowInfo = + createWindowInfo(taskId, 0); + mStartingSurfaceDrawer.addStartingWindow(windowInfo, mBinder); + waitHandlerIdle(mainLoop); + verify(mStartingSurfaceDrawer).postAddWindow(eq(taskId), eq(mBinder), any(), any(), any()); + assertNotEquals(mStartingSurfaceDrawer.mViewThemeResId, 0); + } + + private StartingWindowInfo createWindowInfo(int taskId, int themeResId) { + StartingWindowInfo windowInfo = new StartingWindowInfo(); + final ActivityInfo info = new ActivityInfo(); + info.applicationInfo = new ApplicationInfo(); + info.packageName = "test"; + info.theme = themeResId; + final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.topActivityInfo = info; + taskInfo.taskId = taskId; + windowInfo.taskInfo = taskInfo; + return windowInfo; + } + + private static void waitHandlerIdle(Handler handler) { + handler.runWithScissors(() -> { }, 0 /* timeout */); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java new file mode 100644 index 000000000000..414a0a778d93 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java @@ -0,0 +1,293 @@ +/* + * 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 unittest.src.com.android.wm.shell.startingsurface; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.never; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.eq; + +import android.app.ActivityManager.TaskDescription; +import android.content.ComponentName; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorSpace; +import android.graphics.Point; +import android.graphics.Rect; +import android.hardware.HardwareBuffer; +import android.view.InsetsState; +import android.view.Surface; +import android.view.SurfaceControl; +import android.window.TaskSnapshot; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.startingsurface.TaskSnapshotWindow; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test class for {@link TaskSnapshotWindow}. + * + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class TaskSnapshotWindowTest { + + private TaskSnapshotWindow mWindow; + + private void setupSurface(int width, int height) { + setupSurface(width, height, new Rect(), 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, + new Rect(0, 0, width, height)); + } + + private void setupSurface(int width, int height, Rect contentInsets, int sysuiVis, + int windowFlags, Rect taskBounds) { + // Previously when constructing TaskSnapshots for this test, scale was 1.0f, so to mimic + // this behavior set the taskSize to be the same as the taskBounds width and height. The + // taskBounds passed here are assumed to be the same task bounds as when the snapshot was + // taken. We assume there is no aspect ratio mismatch between the screenshot and the + // taskBounds + assertEquals(width, taskBounds.width()); + assertEquals(height, taskBounds.height()); + Point taskSize = new Point(taskBounds.width(), taskBounds.height()); + + final TaskSnapshot snapshot = createTaskSnapshot(width, height, taskSize, contentInsets); + mWindow = new TaskSnapshotWindow(new SurfaceControl(), snapshot, "Test", + createTaskDescription(Color.WHITE, Color.RED, Color.BLUE), + 0 /* appearance */, windowFlags /* windowFlags */, 0 /* privateWindowFlags */, + taskBounds, ORIENTATION_PORTRAIT, ACTIVITY_TYPE_STANDARD, new InsetsState(), + null /* clearWindow */); + } + + private TaskSnapshot createTaskSnapshot(int width, int height, Point taskSize, + Rect contentInsets) { + final HardwareBuffer buffer = HardwareBuffer.create(width, height, HardwareBuffer.RGBA_8888, + 1, HardwareBuffer.USAGE_CPU_READ_RARELY); + return new TaskSnapshot( + System.currentTimeMillis(), + new ComponentName("", ""), buffer, + ColorSpace.get(ColorSpace.Named.SRGB), ORIENTATION_PORTRAIT, + Surface.ROTATION_0, taskSize, contentInsets, false, + true /* isRealSnapshot */, WINDOWING_MODE_FULLSCREEN, + 0 /* systemUiVisibility */, false /* isTranslucent */, false /* hasImeSurface */); + } + + private static TaskDescription createTaskDescription(int background, int statusBar, + int navigationBar) { + final TaskDescription td = new TaskDescription(); + td.setBackgroundColor(background); + td.setStatusBarColor(statusBar); + td.setNavigationBarColor(navigationBar); + return td; + } + + @Test + public void fillEmptyBackground_fillHorizontally() { + setupSurface(200, 100); + final Canvas mockCanvas = mock(Canvas.class); + when(mockCanvas.getWidth()).thenReturn(200); + when(mockCanvas.getHeight()).thenReturn(100); + mWindow.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 100, 200)); + verify(mockCanvas).drawRect(eq(100.0f), eq(0.0f), eq(200.0f), eq(100.0f), any()); + } + + @Test + public void fillEmptyBackground_fillVertically() { + setupSurface(100, 200); + final Canvas mockCanvas = mock(Canvas.class); + when(mockCanvas.getWidth()).thenReturn(100); + when(mockCanvas.getHeight()).thenReturn(200); + mWindow.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 200, 100)); + verify(mockCanvas).drawRect(eq(0.0f), eq(100.0f), eq(100.0f), eq(200.0f), any()); + } + + @Test + public void fillEmptyBackground_fillBoth() { + setupSurface(200, 200); + final Canvas mockCanvas = mock(Canvas.class); + when(mockCanvas.getWidth()).thenReturn(200); + when(mockCanvas.getHeight()).thenReturn(200); + mWindow.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 100, 100)); + verify(mockCanvas).drawRect(eq(100.0f), eq(0.0f), eq(200.0f), eq(100.0f), any()); + verify(mockCanvas).drawRect(eq(0.0f), eq(100.0f), eq(200.0f), eq(200.0f), any()); + } + + @Test + public void fillEmptyBackground_dontFill_sameSize() { + setupSurface(100, 100); + final Canvas mockCanvas = mock(Canvas.class); + when(mockCanvas.getWidth()).thenReturn(100); + when(mockCanvas.getHeight()).thenReturn(100); + mWindow.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 100, 100)); + verify(mockCanvas, never()).drawRect(anyInt(), anyInt(), anyInt(), anyInt(), any()); + } + + @Test + public void fillEmptyBackground_dontFill_bitmapLarger() { + setupSurface(100, 100); + final Canvas mockCanvas = mock(Canvas.class); + when(mockCanvas.getWidth()).thenReturn(100); + when(mockCanvas.getHeight()).thenReturn(100); + mWindow.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 200, 200)); + verify(mockCanvas, never()).drawRect(anyInt(), anyInt(), anyInt(), anyInt(), any()); + } + + @Test + public void testCalculateSnapshotCrop() { + setupSurface(100, 100, new Rect(0, 10, 0, 10), 0, 0, new Rect(0, 0, 100, 100)); + assertEquals(new Rect(0, 0, 100, 90), mWindow.calculateSnapshotCrop()); + } + + @Test + public void testCalculateSnapshotCrop_taskNotOnTop() { + setupSurface(100, 100, new Rect(0, 10, 0, 10), 0, 0, new Rect(0, 50, 100, 150)); + assertEquals(new Rect(0, 10, 100, 90), mWindow.calculateSnapshotCrop()); + } + + @Test + public void testCalculateSnapshotCrop_navBarLeft() { + setupSurface(100, 100, new Rect(10, 10, 0, 0), 0, 0, new Rect(0, 0, 100, 100)); + assertEquals(new Rect(10, 0, 100, 100), mWindow.calculateSnapshotCrop()); + } + + @Test + public void testCalculateSnapshotCrop_navBarRight() { + setupSurface(100, 100, new Rect(0, 10, 10, 0), 0, 0, new Rect(0, 0, 100, 100)); + assertEquals(new Rect(0, 0, 90, 100), mWindow.calculateSnapshotCrop()); + } + + @Test + public void testCalculateSnapshotCrop_waterfall() { + setupSurface(100, 100, new Rect(5, 10, 5, 10), 0, 0, new Rect(0, 0, 100, 100)); + assertEquals(new Rect(5, 0, 95, 90), mWindow.calculateSnapshotCrop()); + } + + @Test + public void testCalculateSnapshotFrame() { + setupSurface(100, 100); + final Rect insets = new Rect(0, 10, 0, 10); + mWindow.setFrames(new Rect(0, 0, 100, 100), insets); + assertEquals(new Rect(0, 0, 100, 80), + mWindow.calculateSnapshotFrame(new Rect(0, 10, 100, 90))); + } + + @Test + public void testCalculateSnapshotFrame_navBarLeft() { + setupSurface(100, 100); + final Rect insets = new Rect(10, 10, 0, 0); + mWindow.setFrames(new Rect(0, 0, 100, 100), insets); + assertEquals(new Rect(10, 0, 100, 90), + mWindow.calculateSnapshotFrame(new Rect(10, 10, 100, 100))); + } + + @Test + public void testCalculateSnapshotFrame_waterfall() { + setupSurface(100, 100, new Rect(5, 10, 5, 10), 0, 0, new Rect(0, 0, 100, 100)); + final Rect insets = new Rect(0, 10, 0, 10); + mWindow.setFrames(new Rect(5, 0, 95, 100), insets); + assertEquals(new Rect(0, 0, 90, 90), + mWindow.calculateSnapshotFrame(new Rect(5, 0, 95, 90))); + } + + @Test + public void testDrawStatusBarBackground() { + setupSurface(100, 100); + final Rect insets = new Rect(0, 10, 10, 0); + mWindow.setFrames(new Rect(0, 0, 100, 100), insets); + final Canvas mockCanvas = mock(Canvas.class); + when(mockCanvas.getWidth()).thenReturn(100); + when(mockCanvas.getHeight()).thenReturn(100); + mWindow.drawStatusBarBackground(mockCanvas, new Rect(0, 0, 50, 100)); + verify(mockCanvas).drawRect(eq(50.0f), eq(0.0f), eq(90.0f), eq(10.0f), any()); + } + + @Test + public void testDrawStatusBarBackground_nullFrame() { + setupSurface(100, 100); + final Rect insets = new Rect(0, 10, 10, 0); + mWindow.setFrames(new Rect(0, 0, 100, 100), insets); + final Canvas mockCanvas = mock(Canvas.class); + when(mockCanvas.getWidth()).thenReturn(100); + when(mockCanvas.getHeight()).thenReturn(100); + mWindow.drawStatusBarBackground(mockCanvas, null); + verify(mockCanvas).drawRect(eq(0.0f), eq(0.0f), eq(90.0f), eq(10.0f), any()); + } + + @Test + public void testDrawStatusBarBackground_nope() { + setupSurface(100, 100); + final Rect insets = new Rect(0, 10, 10, 0); + mWindow.setFrames(new Rect(0, 0, 100, 100), insets); + final Canvas mockCanvas = mock(Canvas.class); + when(mockCanvas.getWidth()).thenReturn(100); + when(mockCanvas.getHeight()).thenReturn(100); + mWindow.drawStatusBarBackground(mockCanvas, new Rect(0, 0, 100, 100)); + verify(mockCanvas, never()).drawRect(anyInt(), anyInt(), anyInt(), anyInt(), any()); + } + + @Test + public void testDrawNavigationBarBackground() { + final Rect insets = new Rect(0, 10, 0, 10); + setupSurface(100, 100, insets, 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, + new Rect(0, 0, 100, 100)); + mWindow.setFrames(new Rect(0, 0, 100, 100), insets); + final Canvas mockCanvas = mock(Canvas.class); + when(mockCanvas.getWidth()).thenReturn(100); + when(mockCanvas.getHeight()).thenReturn(100); + mWindow.drawNavigationBarBackground(mockCanvas); + verify(mockCanvas).drawRect(eq(new Rect(0, 90, 100, 100)), any()); + } + + @Test + public void testDrawNavigationBarBackground_left() { + final Rect insets = new Rect(10, 10, 0, 0); + setupSurface(100, 100, insets, 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, + new Rect(0, 0, 100, 100)); + mWindow.setFrames(new Rect(0, 0, 100, 100), insets); + final Canvas mockCanvas = mock(Canvas.class); + when(mockCanvas.getWidth()).thenReturn(100); + when(mockCanvas.getHeight()).thenReturn(100); + mWindow.drawNavigationBarBackground(mockCanvas); + verify(mockCanvas).drawRect(eq(new Rect(0, 0, 10, 100)), any()); + } + + @Test + public void testDrawNavigationBarBackground_right() { + final Rect insets = new Rect(0, 10, 10, 0); + setupSurface(100, 100, insets, 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, + new Rect(0, 0, 100, 100)); + mWindow.setFrames(new Rect(0, 0, 100, 100), insets); + final Canvas mockCanvas = mock(Canvas.class); + when(mockCanvas.getWidth()).thenReturn(100); + when(mockCanvas.getHeight()).thenReturn(100); + mWindow.drawNavigationBarBackground(mockCanvas); + verify(mockCanvas).drawRect(eq(new Rect(90, 0, 100, 100)), any()); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java new file mode 100644 index 000000000000..5eca3e75a7b8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -0,0 +1,371 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.transition; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager.RunningTaskInfo; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteException; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.IRemoteTransition; +import android.window.IRemoteTransitionFinishedCallback; +import android.window.TransitionFilter; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; +import android.window.WindowOrganizer; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TransactionPool; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; + +/** + * Tests for the shell transitions. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ShellTransitionTests { + + private final WindowOrganizer mOrganizer = mock(WindowOrganizer.class); + private final TransactionPool mTransactionPool = mock(TransactionPool.class); + private final TestShellExecutor mMainExecutor = new TestShellExecutor(); + private final ShellExecutor mAnimExecutor = new TestShellExecutor(); + private final TestTransitionHandler mDefaultHandler = new TestTransitionHandler(); + + @Before + public void setUp() { + doAnswer(invocation -> invocation.getArguments()[1]) + .when(mOrganizer).startTransition(anyInt(), any(), any()); + } + + @Test + public void testBasicTransitionFlow() { + Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mMainExecutor, + mAnimExecutor); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + IBinder transitToken = new Binder(); + transitions.requestStartTransition(transitToken, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any()); + TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class)); + assertEquals(1, mDefaultHandler.activeCount()); + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + verify(mOrganizer, times(1)).finishTransition(eq(transitToken), any(), any()); + } + + @Test + public void testNonDefaultHandler() { + Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mMainExecutor, + mAnimExecutor); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + final WindowContainerTransaction handlerWCT = new WindowContainerTransaction(); + // Make a test handler that only responds to multi-window triggers AND only animates + // Change transitions. + TestTransitionHandler testHandler = new TestTransitionHandler() { + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + for (TransitionInfo.Change chg : info.getChanges()) { + if (chg.getMode() == TRANSIT_CHANGE) { + return super.startAnimation(transition, info, t, finishCallback); + } + } + return false; + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + final RunningTaskInfo task = request.getTriggerTask(); + return (task != null && task.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) + ? handlerWCT : null; + } + }; + transitions.addHandler(testHandler); + + IBinder transitToken = new Binder(); + TransitionInfo open = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + + // Make a request that will be rejected by the testhandler. + transitions.requestStartTransition(transitToken, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), isNull()); + transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class)); + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(0, testHandler.activeCount()); + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + + // Make a request that will be handled by testhandler but not animated by it. + RunningTaskInfo mwTaskInfo = + createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD); + transitions.requestStartTransition(transitToken, + new TransitionRequestInfo(TRANSIT_OPEN, mwTaskInfo, null /* remote */)); + verify(mOrganizer, times(1)).startTransition( + eq(TRANSIT_OPEN), eq(transitToken), eq(handlerWCT)); + transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class)); + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(0, testHandler.activeCount()); + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + + // Make a request that will be handled AND animated by testhandler. + // Add an aggressive handler (doesn't handle but always animates) on top to make sure that + // the test handler gets first shot at animating since it claimed to handle it. + TestTransitionHandler topHandler = new TestTransitionHandler(); + transitions.addHandler(topHandler); + transitions.requestStartTransition(transitToken, + new TransitionRequestInfo(TRANSIT_CHANGE, mwTaskInfo, null /* remote */)); + verify(mOrganizer, times(1)).startTransition( + eq(TRANSIT_CHANGE), eq(transitToken), eq(handlerWCT)); + TransitionInfo change = new TransitionInfoBuilder(TRANSIT_CHANGE) + .addChange(TRANSIT_CHANGE).build(); + transitions.onTransitionReady(transitToken, change, mock(SurfaceControl.Transaction.class)); + assertEquals(0, mDefaultHandler.activeCount()); + assertEquals(1, testHandler.activeCount()); + assertEquals(0, topHandler.activeCount()); + testHandler.finishAll(); + mMainExecutor.flushAll(); + } + + @Test + public void testRequestRemoteTransition() { + Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mMainExecutor, + mAnimExecutor); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + final boolean[] remoteCalled = new boolean[]{false}; + final WindowContainerTransaction remoteFinishWCT = new WindowContainerTransaction(); + IRemoteTransition testRemote = new IRemoteTransition.Stub() { + @Override + public void startAnimation(TransitionInfo info, SurfaceControl.Transaction t, + IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { + remoteCalled[0] = true; + finishCallback.onTransitionFinished(remoteFinishWCT); + } + }; + IBinder transitToken = new Binder(); + transitions.requestStartTransition(transitToken, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, testRemote)); + verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any()); + TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class)); + assertEquals(0, mDefaultHandler.activeCount()); + assertTrue(remoteCalled[0]); + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + verify(mOrganizer, times(1)).finishTransition(eq(transitToken), eq(remoteFinishWCT), any()); + } + + @Test + public void testTransitionFilterActivityType() { + TransitionFilter filter = new TransitionFilter(); + filter.mRequirements = + new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()}; + filter.mRequirements[0].mActivityType = ACTIVITY_TYPE_HOME; + filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; + + final TransitionInfo openHome = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, + createTaskInfo(1, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_HOME)).build(); + assertTrue(filter.matches(openHome)); + + final TransitionInfo openStd = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, createTaskInfo( + 1, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD)).build(); + assertFalse(filter.matches(openStd)); + } + + @Test + public void testTransitionFilterMultiRequirement() { + // filter that requires at-least one opening and one closing app + TransitionFilter filter = new TransitionFilter(); + filter.mRequirements = new TransitionFilter.Requirement[]{ + new TransitionFilter.Requirement(), new TransitionFilter.Requirement()}; + filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; + filter.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK}; + + final TransitionInfo openOnly = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).build(); + assertFalse(filter.matches(openOnly)); + + final TransitionInfo openClose = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + assertTrue(filter.matches(openClose)); + } + + @Test + public void testRegisteredRemoteTransition() { + Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mMainExecutor, + mAnimExecutor); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + final boolean[] remoteCalled = new boolean[]{false}; + IRemoteTransition testRemote = new IRemoteTransition.Stub() { + @Override + public void startAnimation(TransitionInfo info, SurfaceControl.Transaction t, + IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { + remoteCalled[0] = true; + finishCallback.onTransitionFinished(null /* wct */); + } + }; + + TransitionFilter filter = new TransitionFilter(); + filter.mRequirements = + new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()}; + filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; + + transitions.registerRemote(filter, testRemote); + mMainExecutor.flushAll(); + + IBinder transitToken = new Binder(); + transitions.requestStartTransition(transitToken, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any()); + TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class)); + assertEquals(0, mDefaultHandler.activeCount()); + assertTrue(remoteCalled[0]); + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + verify(mOrganizer, times(1)).finishTransition(eq(transitToken), any(), any()); + } + + class TransitionInfoBuilder { + final TransitionInfo mInfo; + + TransitionInfoBuilder(@WindowManager.TransitionType int type) { + mInfo = new TransitionInfo(type, 0 /* flags */); + mInfo.setRootLeash(createMockSurface(true /* valid */), 0, 0); + } + + TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, + RunningTaskInfo taskInfo) { + final TransitionInfo.Change change = + new TransitionInfo.Change(null /* token */, null /* leash */); + change.setMode(mode); + change.setTaskInfo(taskInfo); + mInfo.addChange(change); + return this; + } + + TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode) { + return addChange(mode, null /* taskInfo */); + } + + TransitionInfo build() { + return mInfo; + } + } + + class TestTransitionHandler implements Transitions.TransitionHandler { + final ArrayList<Transitions.TransitionFinishCallback> mFinishes = new ArrayList<>(); + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + mFinishes.add(finishCallback); + return true; + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + return null; + } + + void finishAll() { + for (int i = mFinishes.size() - 1; i >= 0; --i) { + mFinishes.get(i).onTransitionFinished(null /* wct */, null /* wctCB */); + } + mFinishes.clear(); + } + + int activeCount() { + return mFinishes.size(); + } + } + + private static SurfaceControl createMockSurface(boolean valid) { + SurfaceControl sc = mock(SurfaceControl.class); + doReturn(valid).when(sc).isValid(); + return sc; + } + + private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode, int activityType) { + RunningTaskInfo taskInfo = new RunningTaskInfo(); + taskInfo.taskId = taskId; + taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); + taskInfo.configuration.windowConfiguration.setActivityType(activityType); + return taskInfo; + } + + private static RunningTaskInfo createTaskInfo(int taskId) { + RunningTaskInfo taskInfo = new RunningTaskInfo(); + taskInfo.taskId = taskId; + return taskInfo; + } + +} diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp index 6a7df94331f3..4b4284a0c745 100644 --- a/libs/androidfw/Android.bp +++ b/libs/androidfw/Android.bp @@ -41,6 +41,7 @@ cc_library { "AssetDir.cpp", "AssetManager.cpp", "AssetManager2.cpp", + "AssetsProvider.cpp", "AttributeResolution.cpp", "ChunkIterator.cpp", "ConfigDescription.cpp", @@ -100,6 +101,11 @@ cc_library { "libz", ], }, + linux_glibc: { + srcs: [ + "CursorWindow.cpp", + ], + }, windows: { enabled: true, }, @@ -159,13 +165,22 @@ 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"], + static_libs: common_test_libs + [ + "liblog", + "libz", + ], }, }, data: [ @@ -188,9 +203,38 @@ 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", + ], + }, + darwin: { + // libbinder is not supported on mac + enabled: false, + }, + }, +} diff --git a/libs/androidfw/ApkAssets.cpp b/libs/androidfw/ApkAssets.cpp index cb56a5172a45..ca5981c0dd5c 100755 --- a/libs/androidfw/ApkAssets.cpp +++ b/libs/androidfw/ApkAssets.cpp @@ -16,533 +16,145 @@ #include "androidfw/ApkAssets.h" -#include <algorithm> - #include "android-base/errors.h" -#include "android-base/file.h" #include "android-base/logging.h" -#include "android-base/stringprintf.h" -#include "android-base/unique_fd.h" -#include "android-base/utf8.h" -#include "utils/Compat.h" -#include "ziparchive/zip_archive.h" - -#include "androidfw/Asset.h" -#include "androidfw/Idmap.h" -#include "androidfw/misc.h" -#include "androidfw/Util.h" namespace android { using base::SystemErrorCodeToString; using base::unique_fd; -static const std::string kResourcesArsc("resources.arsc"); - -ApkAssets::ApkAssets(std::unique_ptr<const AssetsProvider> assets_provider, - std::string path, - time_t last_mod_time, - package_property_t property_flags) - : assets_provider_(std::move(assets_provider)), - path_(std::move(path)), - last_mod_time_(last_mod_time), - property_flags_(property_flags) { -} - -// Provides asset files from a zip file. -class ZipAssetsProvider : public AssetsProvider { - public: - ~ZipAssetsProvider() override = default; - - static std::unique_ptr<const AssetsProvider> Create(const std::string& path) { - ::ZipArchiveHandle unmanaged_handle; - const int32_t result = ::OpenArchive(path.c_str(), &unmanaged_handle); - if (result != 0) { - LOG(ERROR) << "Failed to open APK '" << path << "' " << ::ErrorCodeString(result); - ::CloseArchive(unmanaged_handle); - return {}; - } - - return std::unique_ptr<AssetsProvider>(new ZipAssetsProvider(path, path, unmanaged_handle)); - } - - static std::unique_ptr<const AssetsProvider> Create( - unique_fd fd, const std::string& friendly_name, const off64_t offset = 0, - const off64_t length = ApkAssets::kUnknownLength) { - - ::ZipArchiveHandle unmanaged_handle; - const int32_t result = (length == ApkAssets::kUnknownLength) - ? ::OpenArchiveFd(fd.release(), friendly_name.c_str(), &unmanaged_handle) - : ::OpenArchiveFdRange(fd.release(), friendly_name.c_str(), &unmanaged_handle, length, - offset); - - if (result != 0) { - LOG(ERROR) << "Failed to open APK '" << friendly_name << "' through FD with offset " << offset - << " and length " << length << ": " << ::ErrorCodeString(result); - ::CloseArchive(unmanaged_handle); - return {}; - } - - return std::unique_ptr<AssetsProvider>(new ZipAssetsProvider({}, friendly_name, - unmanaged_handle)); - } - - // Iterate over all files and directories within the zip. The order of iteration is not - // guaranteed to be the same as the order of elements in the central directory but is stable for a - // given zip file. - bool ForEachFile(const std::string& root_path, - const std::function<void(const StringPiece&, FileType)>& f) const override { - // If this is a resource loader from an .arsc, there will be no zip handle - if (zip_handle_ == nullptr) { - return false; - } - - std::string root_path_full = root_path; - if (root_path_full.back() != '/') { - root_path_full += '/'; - } - - void* cookie; - if (::StartIteration(zip_handle_.get(), &cookie, root_path_full, "") != 0) { - return false; - } - - std::string name; - ::ZipEntry entry{}; - - // We need to hold back directories because many paths will contain them and we want to only - // surface one. - std::set<std::string> dirs{}; - - int32_t result; - while ((result = ::Next(cookie, &entry, &name)) == 0) { - StringPiece full_file_path(name); - StringPiece leaf_file_path = full_file_path.substr(root_path_full.size()); - - if (!leaf_file_path.empty()) { - auto iter = std::find(leaf_file_path.begin(), leaf_file_path.end(), '/'); - if (iter != leaf_file_path.end()) { - std::string dir = - leaf_file_path.substr(0, std::distance(leaf_file_path.begin(), iter)).to_string(); - dirs.insert(std::move(dir)); - } else { - f(leaf_file_path, kFileTypeRegular); - } - } - } - ::EndIteration(cookie); - - // Now present the unique directories. - for (const std::string& dir : dirs) { - f(dir, kFileTypeDirectory); - } - - // -1 is end of iteration, anything else is an error. - return result == -1; - } - - protected: - std::unique_ptr<Asset> OpenInternal( - const std::string& path, Asset::AccessMode mode, bool* file_exists) const override { - if (file_exists) { - *file_exists = false; - } - - ::ZipEntry entry; - int32_t result = ::FindEntry(zip_handle_.get(), path, &entry); - if (result != 0) { - return {}; - } - - if (file_exists) { - *file_exists = true; - } - - const int fd = ::GetFileDescriptor(zip_handle_.get()); - const off64_t fd_offset = ::GetFileDescriptorOffset(zip_handle_.get()); - incfs::IncFsFileMap asset_map; - if (entry.method == kCompressDeflated) { - if (!asset_map.Create(fd, entry.offset + fd_offset, entry.compressed_length, GetPath())) { - LOG(ERROR) << "Failed to mmap file '" << path << "' in APK '" << friendly_name_ << "'"; - return {}; - } - - std::unique_ptr<Asset> asset = - Asset::createFromCompressedMap(std::move(asset_map), entry.uncompressed_length, mode); - if (asset == nullptr) { - LOG(ERROR) << "Failed to decompress '" << path << "' in APK '" << friendly_name_ << "'"; - return {}; - } - return asset; - } - - if (!asset_map.Create(fd, entry.offset + fd_offset, entry.uncompressed_length, GetPath())) { - LOG(ERROR) << "Failed to mmap file '" << path << "' in APK '" << friendly_name_ << "'"; - return {}; - } - - unique_fd ufd; - if (!GetPath()) { - // If the `path` is not set, create a new `fd` for the new Asset to own in order to create - // new file descriptors using Asset::openFileDescriptor. If the path is set, it will be used - // to create new file descriptors. - ufd = unique_fd(dup(fd)); - if (!ufd.ok()) { - LOG(ERROR) << "Unable to dup fd '" << path << "' in APK '" << friendly_name_ << "'"; - return {}; - } - } - - auto asset = Asset::createFromUncompressedMap(std::move(asset_map), mode, std::move(ufd)); - if (asset == nullptr) { - LOG(ERROR) << "Failed to mmap file '" << path << "' in APK '" << friendly_name_ << "'"; - return {}; - } - return asset; - } - - private: - DISALLOW_COPY_AND_ASSIGN(ZipAssetsProvider); - - explicit ZipAssetsProvider(std::string path, - std::string friendly_name, - ZipArchiveHandle unmanaged_handle) - : zip_handle_(unmanaged_handle, ::CloseArchive), - path_(std::move(path)), - friendly_name_(std::move(friendly_name)) { } - - const char* GetPath() const { - return path_.empty() ? nullptr : path_.c_str(); - } - - using ZipArchivePtr = std::unique_ptr<ZipArchive, void (*)(ZipArchiveHandle)>; - ZipArchivePtr zip_handle_; - std::string path_; - std::string friendly_name_; -}; - -class DirectoryAssetsProvider : AssetsProvider { - public: - ~DirectoryAssetsProvider() override = default; - - static std::unique_ptr<const AssetsProvider> Create(const std::string& path) { - struct stat sb{}; - const int result = stat(path.c_str(), &sb); - if (result == -1) { - LOG(ERROR) << "Failed to find directory '" << path << "'."; - return nullptr; - } - - if (!S_ISDIR(sb.st_mode)) { - LOG(ERROR) << "Path '" << path << "' is not a directory."; - return nullptr; - } - - return std::unique_ptr<AssetsProvider>(new DirectoryAssetsProvider(path)); - } - - protected: - std::unique_ptr<Asset> OpenInternal( - const std::string& path, Asset::AccessMode /* mode */, bool* file_exists) const override { - const std::string resolved_path = ResolvePath(path); - if (file_exists) { - struct stat sb{}; - const int result = stat(resolved_path.c_str(), &sb); - *file_exists = result != -1 && S_ISREG(sb.st_mode); - } - - return ApkAssets::CreateAssetFromFile(resolved_path); - } - - private: - DISALLOW_COPY_AND_ASSIGN(DirectoryAssetsProvider); - - explicit DirectoryAssetsProvider(std::string path) : path_(std::move(path)) { } - - inline std::string ResolvePath(const std::string& path) const { - return base::StringPrintf("%s%c%s", path_.c_str(), OS_PATH_SEPARATOR, path.c_str()); - } - - const std::string path_; -}; - -// AssetProvider implementation that does not provide any assets. Used for ApkAssets::LoadEmpty. -class EmptyAssetsProvider : public AssetsProvider { - public: - EmptyAssetsProvider() = default; - ~EmptyAssetsProvider() override = default; - - protected: - std::unique_ptr<Asset> OpenInternal(const std::string& /*path */, - Asset::AccessMode /* mode */, - bool* file_exists) const override { - if (file_exists) { - *file_exists = false; - } - return nullptr; - } - - private: - DISALLOW_COPY_AND_ASSIGN(EmptyAssetsProvider); -}; - -// AssetProvider implementation -class MultiAssetsProvider : public AssetsProvider { - public: - ~MultiAssetsProvider() override = default; - - static std::unique_ptr<const AssetsProvider> Create( - std::unique_ptr<const AssetsProvider> child, std::unique_ptr<const AssetsProvider> parent) { - CHECK(parent != nullptr) << "parent provider must not be null"; - return (!child) ? std::move(parent) - : std::unique_ptr<const AssetsProvider>(new MultiAssetsProvider( - std::move(child), std::move(parent))); - } - - bool ForEachFile(const std::string& root_path, - const std::function<void(const StringPiece&, FileType)>& f) const override { - // TODO: Only call the function once for files defined in the parent and child - return child_->ForEachFile(root_path, f) && parent_->ForEachFile(root_path, f); - } - - protected: - std::unique_ptr<Asset> OpenInternal( - const std::string& path, Asset::AccessMode mode, bool* file_exists) const override { - auto asset = child_->Open(path, mode, file_exists); - return (asset) ? std::move(asset) : parent_->Open(path, mode, file_exists); - } - - private: - DISALLOW_COPY_AND_ASSIGN(MultiAssetsProvider); - - MultiAssetsProvider(std::unique_ptr<const AssetsProvider> child, - std::unique_ptr<const AssetsProvider> parent) - : child_(std::move(child)), parent_(std::move(parent)) { } - - std::unique_ptr<const AssetsProvider> child_; - std::unique_ptr<const AssetsProvider> parent_; -}; - -// Opens the archive using the file path. Calling CloseArchive on the zip handle will close the -// file. -std::unique_ptr<const ApkAssets> ApkAssets::Load( - const std::string& path, const package_property_t flags, - std::unique_ptr<const AssetsProvider> override_asset) { - auto assets = ZipAssetsProvider::Create(path); - return (assets) ? LoadImpl(std::move(assets), path, flags, std::move(override_asset)) - : nullptr; +constexpr const char* kResourcesArsc = "resources.arsc"; + +ApkAssets::ApkAssets(std::unique_ptr<Asset> resources_asset, + std::unique_ptr<LoadedArsc> loaded_arsc, + std::unique_ptr<AssetsProvider> assets, + package_property_t property_flags, + std::unique_ptr<Asset> idmap_asset, + std::unique_ptr<LoadedIdmap> loaded_idmap) + : resources_asset_(std::move(resources_asset)), + loaded_arsc_(std::move(loaded_arsc)), + assets_provider_(std::move(assets)), + property_flags_(property_flags), + idmap_asset_(std::move(idmap_asset)), + loaded_idmap_(std::move(loaded_idmap)) {} + +std::unique_ptr<ApkAssets> ApkAssets::Load(const std::string& path, package_property_t flags) { + return Load(ZipAssetsProvider::Create(path), flags); } -// Opens the archive using the file file descriptor with the specified file offset and read length. -// If the `assume_ownership` parameter is 'true' calling CloseArchive will close the file. -std::unique_ptr<const ApkAssets> ApkAssets::LoadFromFd( - unique_fd fd, const std::string& friendly_name, const package_property_t flags, - std::unique_ptr<const AssetsProvider> override_asset, const off64_t offset, - const off64_t length) { - CHECK(length >= kUnknownLength) << "length must be greater than or equal to " << kUnknownLength; - CHECK(length != kUnknownLength || offset == 0) << "offset must be 0 if length is " - << kUnknownLength; - - auto assets = ZipAssetsProvider::Create(std::move(fd), friendly_name, offset, length); - return (assets) ? LoadImpl(std::move(assets), friendly_name, flags, std::move(override_asset)) - : nullptr; +std::unique_ptr<ApkAssets> ApkAssets::LoadFromFd(base::unique_fd fd, + const std::string& debug_name, + package_property_t flags, + off64_t offset, + off64_t len) { + return Load(ZipAssetsProvider::Create(std::move(fd), debug_name, offset, len), flags); } -std::unique_ptr<const ApkAssets> ApkAssets::LoadTable( - const std::string& path, const package_property_t flags, - std::unique_ptr<const AssetsProvider> override_asset) { - - auto assets = CreateAssetFromFile(path); - return (assets) ? LoadTableImpl(std::move(assets), path, flags, std::move(override_asset)) - : nullptr; +std::unique_ptr<ApkAssets> ApkAssets::Load(std::unique_ptr<AssetsProvider> assets, + package_property_t flags) { + return LoadImpl(std::move(assets), flags, nullptr /* idmap_asset */, nullptr /* loaded_idmap */); } -std::unique_ptr<const ApkAssets> ApkAssets::LoadTableFromFd( - unique_fd fd, const std::string& friendly_name, const package_property_t flags, - std::unique_ptr<const AssetsProvider> override_asset, const off64_t offset, - const off64_t length) { - - auto assets = CreateAssetFromFd(std::move(fd), nullptr /* path */, offset, length); - return (assets) ? LoadTableImpl(std::move(assets), friendly_name, flags, - std::move(override_asset)) - : nullptr; +std::unique_ptr<ApkAssets> ApkAssets::LoadTable(std::unique_ptr<Asset> resources_asset, + std::unique_ptr<AssetsProvider> assets, + package_property_t flags) { + if (resources_asset == nullptr) { + return {}; + } + return LoadImpl(std::move(resources_asset), std::move(assets), flags, nullptr /* idmap_asset */, + nullptr /* loaded_idmap */); } -std::unique_ptr<const ApkAssets> ApkAssets::LoadOverlay(const std::string& idmap_path, - const package_property_t flags) { +std::unique_ptr<ApkAssets> ApkAssets::LoadOverlay(const std::string& idmap_path, + package_property_t flags) { CHECK((flags & PROPERTY_LOADER) == 0U) << "Cannot load RROs through loaders"; - std::unique_ptr<Asset> idmap_asset = CreateAssetFromFile(idmap_path); + auto idmap_asset = AssetsProvider::CreateAssetFromFile(idmap_path); if (idmap_asset == nullptr) { + LOG(ERROR) << "failed to read IDMAP " << idmap_path; return {}; } - const StringPiece idmap_data( - reinterpret_cast<const char*>(idmap_asset->getBuffer(true /*wordAligned*/)), - static_cast<size_t>(idmap_asset->getLength())); - std::unique_ptr<const LoadedIdmap> loaded_idmap = LoadedIdmap::Load(idmap_path, idmap_data); + StringPiece idmap_data(reinterpret_cast<const char*>(idmap_asset->getBuffer(true /* aligned */)), + static_cast<size_t>(idmap_asset->getLength())); + auto loaded_idmap = LoadedIdmap::Load(idmap_path, idmap_data); if (loaded_idmap == nullptr) { LOG(ERROR) << "failed to load IDMAP " << idmap_path; return {}; } - - auto overlay_path = loaded_idmap->OverlayApkPath(); - auto assets = ZipAssetsProvider::Create(overlay_path); - return (assets) ? LoadImpl(std::move(assets), overlay_path, flags | PROPERTY_OVERLAY, - nullptr /* override_asset */, std::move(idmap_asset), - std::move(loaded_idmap)) - : nullptr; -} - -std::unique_ptr<const ApkAssets> ApkAssets::LoadFromDir( - const std::string& path, const package_property_t flags, - std::unique_ptr<const AssetsProvider> override_asset) { - - auto assets = DirectoryAssetsProvider::Create(path); - return (assets) ? LoadImpl(std::move(assets), path, flags, std::move(override_asset)) - : nullptr; -} - -std::unique_ptr<const ApkAssets> ApkAssets::LoadEmpty( - const package_property_t flags, std::unique_ptr<const AssetsProvider> override_asset) { - - auto assets = (override_asset) ? std::move(override_asset) - : std::unique_ptr<const AssetsProvider>(new EmptyAssetsProvider()); - std::unique_ptr<ApkAssets> loaded_apk(new ApkAssets(std::move(assets), "empty" /* path */, - -1 /* last_mod-time */, flags)); - loaded_apk->loaded_arsc_ = LoadedArsc::CreateEmpty(); - // Need to force a move for mingw32. - return std::move(loaded_apk); -} -std::unique_ptr<Asset> ApkAssets::CreateAssetFromFile(const std::string& path) { - unique_fd fd(base::utf8::open(path.c_str(), O_RDONLY | O_BINARY | O_CLOEXEC)); - if (!fd.ok()) { - LOG(ERROR) << "Failed to open file '" << path << "': " << SystemErrorCodeToString(errno); + const std::string overlay_path(loaded_idmap->OverlayApkPath()); + auto overlay_assets = ZipAssetsProvider::Create(overlay_path); + if (overlay_assets == nullptr) { return {}; } - return CreateAssetFromFd(std::move(fd), path.c_str()); + return LoadImpl(std::move(overlay_assets), flags | PROPERTY_OVERLAY, std::move(idmap_asset), + std::move(loaded_idmap)); } -std::unique_ptr<Asset> ApkAssets::CreateAssetFromFd(base::unique_fd fd, - const char* path, - off64_t offset, - off64_t length) { - CHECK(length >= kUnknownLength) << "length must be greater than or equal to " << kUnknownLength; - CHECK(length != kUnknownLength || offset == 0) << "offset must be 0 if length is " - << kUnknownLength; - if (length == kUnknownLength) { - length = lseek64(fd, 0, SEEK_END); - if (length < 0) { - LOG(ERROR) << "Failed to get size of file '" << ((path) ? path : "anon") << "': " - << SystemErrorCodeToString(errno); - return {}; - } - } - - incfs::IncFsFileMap file_map; - if (!file_map.Create(fd, offset, static_cast<size_t>(length), path)) { - LOG(ERROR) << "Failed to mmap file '" << ((path) ? path : "anon") << "': " - << SystemErrorCodeToString(errno); +std::unique_ptr<ApkAssets> ApkAssets::LoadImpl(std::unique_ptr<AssetsProvider> assets, + package_property_t property_flags, + std::unique_ptr<Asset> idmap_asset, + std::unique_ptr<LoadedIdmap> loaded_idmap) { + if (assets == nullptr) { return {}; } - // If `path` is set, do not pass ownership of the `fd` to the new Asset since - // Asset::openFileDescriptor can use `path` to create new file descriptors. - return Asset::createFromUncompressedMap(std::move(file_map), - Asset::AccessMode::ACCESS_RANDOM, - (path) ? base::unique_fd(-1) : std::move(fd)); -} - -std::unique_ptr<const ApkAssets> ApkAssets::LoadImpl( - std::unique_ptr<const AssetsProvider> assets, const std::string& path, - package_property_t property_flags, std::unique_ptr<const AssetsProvider> override_assets, - std::unique_ptr<Asset> idmap_asset, std::unique_ptr<const LoadedIdmap> idmap) { - - const time_t last_mod_time = getFileModDate(path.c_str()); - // Open the resource table via mmap unless it is compressed. This logic is taken care of by Open. bool resources_asset_exists = false; - auto resources_asset_ = assets->Open(kResourcesArsc, Asset::AccessMode::ACCESS_BUFFER, - &resources_asset_exists); - - assets = MultiAssetsProvider::Create(std::move(override_assets), std::move(assets)); - - // Wrap the handle in a unique_ptr so it gets automatically closed. - std::unique_ptr<ApkAssets> - loaded_apk(new ApkAssets(std::move(assets), path, last_mod_time, property_flags)); - - if (!resources_asset_exists) { - loaded_apk->loaded_arsc_ = LoadedArsc::CreateEmpty(); - return std::move(loaded_apk); - } - - loaded_apk->resources_asset_ = std::move(resources_asset_); - if (!loaded_apk->resources_asset_) { - LOG(ERROR) << "Failed to open '" << kResourcesArsc << "' in APK '" << path << "'."; + auto resources_asset = assets->Open(kResourcesArsc, Asset::AccessMode::ACCESS_BUFFER, + &resources_asset_exists); + if (resources_asset == nullptr && resources_asset_exists) { + LOG(ERROR) << "Failed to open '" << kResourcesArsc << "' in APK '" << assets->GetDebugName() + << "'."; return {}; } - // Must retain ownership of the IDMAP Asset so that all pointers to its mmapped data remain valid. - loaded_apk->idmap_asset_ = std::move(idmap_asset); - loaded_apk->loaded_idmap_ = std::move(idmap); + return LoadImpl(std::move(resources_asset), std::move(assets), property_flags, + std::move(idmap_asset), std::move(loaded_idmap)); +} - const auto data = loaded_apk->resources_asset_->getIncFsBuffer(true /* aligned */); - const size_t length = loaded_apk->resources_asset_->getLength(); - if (!data || length == 0) { - LOG(ERROR) << "Failed to read '" << kResourcesArsc << "' data in APK '" << path << "'."; +std::unique_ptr<ApkAssets> ApkAssets::LoadImpl(std::unique_ptr<Asset> resources_asset, + std::unique_ptr<AssetsProvider> assets, + package_property_t property_flags, + std::unique_ptr<Asset> idmap_asset, + std::unique_ptr<LoadedIdmap> loaded_idmap) { + if (assets == nullptr ) { return {}; } - loaded_apk->loaded_arsc_ = LoadedArsc::Load(data, length, loaded_apk->loaded_idmap_.get(), - property_flags); - if (!loaded_apk->loaded_arsc_) { - LOG(ERROR) << "Failed to load '" << kResourcesArsc << "' in APK '" << path << "'."; - return {}; + std::unique_ptr<LoadedArsc> loaded_arsc; + if (resources_asset != nullptr) { + const auto data = resources_asset->getIncFsBuffer(true /* aligned */); + const size_t length = resources_asset->getLength(); + if (!data || length == 0) { + LOG(ERROR) << "Failed to read resources table in APK '" << assets->GetDebugName() << "'."; + return {}; + } + loaded_arsc = LoadedArsc::Load(data, length, loaded_idmap.get(), property_flags); + } else { + loaded_arsc = LoadedArsc::CreateEmpty(); } - // Need to force a move for mingw32. - return std::move(loaded_apk); -} - -std::unique_ptr<const ApkAssets> ApkAssets::LoadTableImpl( - std::unique_ptr<Asset> resources_asset, const std::string& path, - package_property_t property_flags, std::unique_ptr<const AssetsProvider> override_assets) { - - const time_t last_mod_time = getFileModDate(path.c_str()); - - auto assets = (override_assets) ? std::move(override_assets) - : std::unique_ptr<AssetsProvider>(new EmptyAssetsProvider()); - - std::unique_ptr<ApkAssets> loaded_apk( - new ApkAssets(std::move(assets), path, last_mod_time, property_flags)); - loaded_apk->resources_asset_ = std::move(resources_asset); - - const auto data = loaded_apk->resources_asset_->getIncFsBuffer(true /* aligned */); - const size_t length = loaded_apk->resources_asset_->getLength(); - if (!data || length == 0) { - LOG(ERROR) << "Failed to read resources table data in '" << path << "'."; + if (loaded_arsc == nullptr) { + LOG(ERROR) << "Failed to load resources table in APK '" << assets->GetDebugName() << "'."; return {}; } - loaded_apk->loaded_arsc_ = LoadedArsc::Load(data, length, nullptr /* loaded_idmap */, - property_flags); - if (loaded_apk->loaded_arsc_ == nullptr) { - LOG(ERROR) << "Failed to read resources table in '" << path << "'."; - return {}; - } + return std::unique_ptr<ApkAssets>(new ApkAssets(std::move(resources_asset), + std::move(loaded_arsc), std::move(assets), + property_flags, std::move(idmap_asset), + std::move(loaded_idmap))); +} - // Need to force a move for mingw32. - return std::move(loaded_apk); +const std::string& ApkAssets::GetPath() const { + return assets_provider_->GetDebugName(); } bool ApkAssets::IsUpToDate() const { - if (IsLoader()) { - // Loaders are invalidated by the app, not the system, so assume they are up to date. - return true; - } - return (!loaded_idmap_ || loaded_idmap_->IsUpToDate()) && - last_mod_time_ == getFileModDate(path_.c_str()); + // Loaders are invalidated by the app, not the system, so assume they are up to date. + return IsLoader() || ((!loaded_idmap_ || loaded_idmap_->IsUpToDate()) + && assets_provider_->IsUpToDate()); } - } // namespace android diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index bec80a7d605e..03ab62f48870 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -103,10 +103,10 @@ AssetManager2::AssetManager2() { } bool AssetManager2::SetApkAssets(const std::vector<const ApkAssets*>& apk_assets, - bool invalidate_caches, bool filter_incompatible_configs) { + bool invalidate_caches) { apk_assets_ = apk_assets; BuildDynamicRefTable(); - RebuildFilterList(filter_incompatible_configs); + RebuildFilterList(); if (invalidate_caches) { InvalidateCaches(static_cast<uint32_t>(-1)); } @@ -157,7 +157,8 @@ void AssetManager2::BuildDynamicRefTable() { // The target package must precede the overlay package in the apk assets paths in order // to take effect. const auto& loaded_idmap = apk_assets->GetLoadedIdmap(); - auto target_package_iter = apk_assets_package_ids.find(loaded_idmap->TargetApkPath()); + auto target_package_iter = apk_assets_package_ids.find( + std::string(loaded_idmap->TargetApkPath())); if (target_package_iter == apk_assets_package_ids.end()) { LOG(INFO) << "failed to find target package for overlay " << loaded_idmap->OverlayApkPath(); @@ -622,7 +623,8 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry( if (UNLIKELY(logging_enabled)) { last_resolution_.steps.push_back( Resolution::Step{Resolution::Step::Type::OVERLAID, overlay_result->config.toString(), - overlay_result->package_name}); + overlay_result->package_name, + overlay_result->cookie}); } } } @@ -645,22 +647,21 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntryInternal( const LoadedPackage* best_package = nullptr; incfs::verified_map_ptr<ResTable_type> best_type; const ResTable_config* best_config = nullptr; - ResTable_config best_config_copy; uint32_t best_offset = 0U; uint32_t type_flags = 0U; - auto resolution_type = Resolution::Step::Type::NO_ENTRY; std::vector<Resolution::Step> resolution_steps; - // If desired_config is the same as the set configuration, then we can use our filtered list - // and we don't need to match the configurations, since they already matched. - const bool use_fast_path = !ignore_configuration && &desired_config == &configuration_; + // If `desired_config` is not the same as the set configuration or the caller will accept a value + // from any configuration, then we cannot use our filtered list of types since it only it contains + // types matched to the set configuration. + const bool use_filtered = !ignore_configuration && &desired_config == &configuration_; const size_t package_count = package_group.packages_.size(); for (size_t pi = 0; pi < package_count; pi++) { const ConfiguredPackage& loaded_package_impl = package_group.packages_[pi]; const LoadedPackage* loaded_package = loaded_package_impl.loaded_package_; - ApkAssetsCookie cookie = package_group.cookies_[pi]; + const ApkAssetsCookie cookie = package_group.cookies_[pi]; // If the type IDs are offset in this package, we need to take that into account when searching // for a type. @@ -669,130 +670,82 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntryInternal( continue; } + // Allow custom loader packages to overlay resource values with configurations equivalent to the + // current best configuration. + const bool package_is_loader = loaded_package->IsCustomLoader(); + auto entry_flags = type_spec->GetFlagsForEntryIndex(entry_idx); if (UNLIKELY(!entry_flags.has_value())) { return base::unexpected(entry_flags.error()); } type_flags |= entry_flags.value(); - // If the package is an overlay or custom loader, - // then even configurations that are the same MUST be chosen. - const bool package_is_loader = loaded_package->IsCustomLoader(); - - if (use_fast_path) { - const FilteredConfigGroup& filtered_group = loaded_package_impl.filtered_configs_[type_idx]; - for (const auto& type_config : filtered_group.type_configs) { - const ResTable_config& this_config = type_config.config; - - // We can skip calling ResTable_config::match() because we know that all candidate - // configurations that do NOT match have been filtered-out. - if (best_config == nullptr) { - resolution_type = Resolution::Step::Type::INITIAL; - } else if (this_config.isBetterThan(*best_config, &desired_config)) { - resolution_type = (package_is_loader) ? Resolution::Step::Type::BETTER_MATCH_LOADER - : Resolution::Step::Type::BETTER_MATCH; - } else if (package_is_loader && this_config.compare(*best_config) == 0) { - resolution_type = Resolution::Step::Type::OVERLAID_LOADER; - } else { - if (UNLIKELY(logging_enabled)) { - resolution_type = (package_is_loader) ? Resolution::Step::Type::SKIPPED_LOADER - : Resolution::Step::Type::SKIPPED; - resolution_steps.push_back(Resolution::Step{resolution_type, - this_config.toString(), - &loaded_package->GetPackageName()}); - } - continue; - } - - // The configuration matches and is better than the previous selection. - // Find the entry value if it exists for this configuration. - const auto& type = type_config.type; - const auto offset = LoadedPackage::GetEntryOffset(type, entry_idx); - if (UNLIKELY(IsIOError(offset))) { - return base::unexpected(offset.error()); - } - if (!offset.has_value()) { - if (UNLIKELY(logging_enabled)) { - if (package_is_loader) { - resolution_type = Resolution::Step::Type::NO_ENTRY_LOADER; - } else { - resolution_type = Resolution::Step::Type::NO_ENTRY; - } - resolution_steps.push_back(Resolution::Step{resolution_type, - this_config.toString(), - &loaded_package->GetPackageName()}); - } - continue; - } - - best_cookie = cookie; - best_package = loaded_package; - best_type = type; - best_config = &this_config; - best_offset = offset.value(); + const FilteredConfigGroup& filtered_group = loaded_package_impl.filtered_configs_[type_idx]; + const size_t type_entry_count = (use_filtered) ? filtered_group.type_entries.size() + : type_spec->type_entries.size(); + for (size_t i = 0; i < type_entry_count; i++) { + const TypeSpec::TypeEntry* type_entry = (use_filtered) ? filtered_group.type_entries[i] + : &type_spec->type_entries[i]; + + // We can skip calling ResTable_config::match() if the caller does not care for the + // configuration to match or if we're using the list of types that have already had their + // configuration matched. + const ResTable_config& this_config = type_entry->config; + if (!(use_filtered || ignore_configuration || this_config.match(desired_config))) { + continue; + } + Resolution::Step::Type resolution_type; + if (best_config == nullptr) { + resolution_type = Resolution::Step::Type::INITIAL; + } else if (this_config.isBetterThan(*best_config, &desired_config)) { + resolution_type = Resolution::Step::Type::BETTER_MATCH; + } else if (package_is_loader && this_config.compare(*best_config) == 0) { + resolution_type = Resolution::Step::Type::OVERLAID; + } else { if (UNLIKELY(logging_enabled)) { - last_resolution_.steps.push_back(Resolution::Step{resolution_type, - this_config.toString(), - &loaded_package->GetPackageName()}); + resolution_steps.push_back(Resolution::Step{Resolution::Step::Type::SKIPPED, + this_config.toString(), + &loaded_package->GetPackageName(), + cookie}); } + continue; } - } else { - // This is the slower path, which doesn't use the filtered list of configurations. - // Here we must read the ResTable_config from the mmapped APK, convert it to host endianness - // and fill in any new fields that did not exist when the APK was compiled. - // Furthermore when selecting configurations we can't just record the pointer to the - // ResTable_config, we must copy it. - const auto iter_end = type_spec->types + type_spec->type_count; - for (auto iter = type_spec->types; iter != iter_end; ++iter) { - const incfs::verified_map_ptr<ResTable_type>& type = *iter; - - ResTable_config this_config{}; - if (!ignore_configuration) { - this_config.copyFromDtoH(type->config); - if (!this_config.match(desired_config)) { - continue; - } - if (best_config == nullptr) { - resolution_type = Resolution::Step::Type::INITIAL; - } else if (this_config.isBetterThan(*best_config, &desired_config)) { - resolution_type = (package_is_loader) ? Resolution::Step::Type::BETTER_MATCH_LOADER - : Resolution::Step::Type::BETTER_MATCH; - } else if (package_is_loader && this_config.compare(*best_config) == 0) { - resolution_type = Resolution::Step::Type::OVERLAID_LOADER; - } else { - continue; - } - } + // The configuration matches and is better than the previous selection. + // Find the entry value if it exists for this configuration. + const auto& type = type_entry->type; + const auto offset = LoadedPackage::GetEntryOffset(type, entry_idx); + if (UNLIKELY(IsIOError(offset))) { + return base::unexpected(offset.error()); + } - // The configuration matches and is better than the previous selection. - // Find the entry value if it exists for this configuration. - const auto offset = LoadedPackage::GetEntryOffset(type, entry_idx); - if (UNLIKELY(IsIOError(offset))) { - return base::unexpected(offset.error()); - } - if (!offset.has_value()) { - continue; + if (!offset.has_value()) { + if (UNLIKELY(logging_enabled)) { + resolution_steps.push_back(Resolution::Step{Resolution::Step::Type::NO_ENTRY, + this_config.toString(), + &loaded_package->GetPackageName(), + cookie}); } + continue; + } - best_cookie = cookie; - best_package = loaded_package; - best_type = type; - best_config_copy = this_config; - best_config = &best_config_copy; - best_offset = offset.value(); + best_cookie = cookie; + best_package = loaded_package; + best_type = type; + best_config = &this_config; + best_offset = offset.value(); - if (stop_at_first_match) { - // Any configuration will suffice, so break. - break; - } + if (UNLIKELY(logging_enabled)) { + last_resolution_.steps.push_back(Resolution::Step{resolution_type, + this_config.toString(), + &loaded_package->GetPackageName(), + cookie}); + } - if (UNLIKELY(logging_enabled)) { - last_resolution_.steps.push_back(Resolution::Step{resolution_type, - this_config.toString(), - &loaded_package->GetPackageName()}); - } + // Any configuration will suffice, so break. + if (stop_at_first_match) { + break; } } } @@ -850,19 +803,16 @@ std::string AssetManager2::GetLastResourceResolution() const { return {}; } - auto cookie = last_resolution_.cookie; + const ApkAssetsCookie cookie = last_resolution_.cookie; if (cookie == kInvalidCookie) { LOG(ERROR) << "AssetManager hasn't resolved a resource to read resolution path."; return {}; } - uint32_t resid = last_resolution_.resid; - std::vector<Resolution::Step>& steps = last_resolution_.steps; - std::string resource_name_string; - - const LoadedPackage* package = - apk_assets_[cookie]->GetLoadedArsc()->GetPackageById(get_package_id(resid)); + const uint32_t resid = last_resolution_.resid; + const auto package = apk_assets_[cookie]->GetLoadedArsc()->GetPackageById(get_package_id(resid)); + std::string resource_name_string; if (package != nullptr) { auto resource_name = ToResourceName(last_resolution_.type_string_ref, last_resolution_.entry_string_ref, @@ -877,44 +827,24 @@ std::string AssetManager2::GetLastResourceResolution() const { << "\n\tFor config -" << configuration_.toString(); - std::string prefix; - for (Resolution::Step step : steps) { - switch (step.type) { - case Resolution::Step::Type::INITIAL: - prefix = "Found initial"; - break; - case Resolution::Step::Type::BETTER_MATCH: - prefix = "Found better"; - break; - case Resolution::Step::Type::BETTER_MATCH_LOADER: - prefix = "Found better in loader"; - break; - case Resolution::Step::Type::OVERLAID: - prefix = "Overlaid"; - break; - case Resolution::Step::Type::OVERLAID_LOADER: - prefix = "Overlaid by loader"; - break; - case Resolution::Step::Type::SKIPPED: - prefix = "Skipped"; - break; - case Resolution::Step::Type::SKIPPED_LOADER: - prefix = "Skipped loader"; - break; - case Resolution::Step::Type::NO_ENTRY: - prefix = "No entry"; - break; - case Resolution::Step::Type::NO_ENTRY_LOADER: - prefix = "No entry for loader"; - break; - } + for (const Resolution::Step& step : last_resolution_.steps) { + const static std::unordered_map<Resolution::Step::Type, const char*> kStepStrings = { + {Resolution::Step::Type::INITIAL, "Found initial"}, + {Resolution::Step::Type::BETTER_MATCH, "Found better"}, + {Resolution::Step::Type::OVERLAID, "Overlaid"}, + {Resolution::Step::Type::SKIPPED, "Skipped"}, + {Resolution::Step::Type::NO_ENTRY, "No entry"} + }; - if (!prefix.empty()) { - log_stream << "\n\t" << prefix << ": " << *step.package_name; + const auto prefix = kStepStrings.find(step.type); + if (prefix == kStepStrings.end()) { + continue; + } - if (!step.config_name.isEmpty()) { - log_stream << " -" << step.config_name; - } + log_stream << "\n\t" << prefix->second << ": " << *step.package_name << " (" + << apk_assets_[step.cookie]->GetPath() << ")"; + if (!step.config_name.isEmpty()) { + log_stream << " -" << step.config_name; } } @@ -934,6 +864,16 @@ base::expected<AssetManager2::ResourceName, NullOrIOError> AssetManager2::GetRes *result->package_name); } +base::expected<uint32_t, NullOrIOError> AssetManager2::GetResourceTypeSpecFlags( + uint32_t resid) const { + auto result = FindEntry(resid, 0u /* density_override */, false /* stop_at_first_match */, + true /* ignore_configuration */); + if (!result.has_value()) { + return base::unexpected(result.error()); + } + return result->type_flags; +} + base::expected<AssetManager2::SelectedValue, NullOrIOError> AssetManager2::GetResource( uint32_t resid, bool may_be_bag, uint16_t density_override) const { auto result = FindEntry(resid, density_override, false /* stop_at_first_match */, @@ -1332,7 +1272,7 @@ base::expected<uint32_t, NullOrIOError> AssetManager2::GetResourceId( return base::unexpected(std::nullopt); } -void AssetManager2::RebuildFilterList(bool filter_incompatible_configs) { +void AssetManager2::RebuildFilterList() { for (PackageGroup& group : package_groups_) { for (ConfiguredPackage& impl : group.packages_) { // Destroy it. @@ -1342,14 +1282,11 @@ void AssetManager2::RebuildFilterList(bool filter_incompatible_configs) { new (&impl.filtered_configs_) ByteBucketArray<FilteredConfigGroup>(); // Create the filters here. - impl.loaded_package_->ForEachTypeSpec([&](const TypeSpec* spec, uint8_t type_index) { - FilteredConfigGroup& group = impl.filtered_configs_.editItemAt(type_index); - const auto iter_end = spec->types + spec->type_count; - for (auto iter = spec->types; iter != iter_end; ++iter) { - ResTable_config this_config; - this_config.copyFromDtoH((*iter)->config); - if (!filter_incompatible_configs || this_config.match(configuration_)) { - group.type_configs.push_back(TypeConfig{*iter, this_config}); + impl.loaded_package_->ForEachTypeSpec([&](const TypeSpec& type_spec, uint8_t type_id) { + FilteredConfigGroup& group = impl.filtered_configs_.editItemAt(type_id - 1); + for (const auto& type_entry : type_spec.type_entries) { + if (type_entry.config.match(configuration_)) { + group.type_entries.push_back(&type_entry); } } }); diff --git a/libs/androidfw/AssetsProvider.cpp b/libs/androidfw/AssetsProvider.cpp new file mode 100644 index 000000000000..23cacf88a6db --- /dev/null +++ b/libs/androidfw/AssetsProvider.cpp @@ -0,0 +1,398 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "androidfw/AssetsProvider.h" + +#include <sys/stat.h> + +#include <android-base/errors.h> +#include <android-base/stringprintf.h> +#include <android-base/utf8.h> +#include <ziparchive/zip_archive.h> + +namespace android { +namespace { +constexpr const char* kEmptyDebugString = "<empty>"; +} // namespace + +std::unique_ptr<Asset> AssetsProvider::Open(const std::string& path, Asset::AccessMode mode, + bool* file_exists) const { + return OpenInternal(path, mode, file_exists); +} + +std::unique_ptr<Asset> AssetsProvider::CreateAssetFromFile(const std::string& path) { + base::unique_fd fd(base::utf8::open(path.c_str(), O_RDONLY | O_CLOEXEC)); + if (!fd.ok()) { + LOG(ERROR) << "Failed to open file '" << path << "': " << base::SystemErrorCodeToString(errno); + return {}; + } + + return CreateAssetFromFd(std::move(fd), path.c_str()); +} + +std::unique_ptr<Asset> AssetsProvider::CreateAssetFromFd(base::unique_fd fd, + const char* path, + off64_t offset, + off64_t length) { + CHECK(length >= kUnknownLength) << "length must be greater than or equal to " << kUnknownLength; + CHECK(length != kUnknownLength || offset == 0) << "offset must be 0 if length is " + << kUnknownLength; + if (length == kUnknownLength) { + length = lseek64(fd, 0, SEEK_END); + if (length < 0) { + LOG(ERROR) << "Failed to get size of file '" << ((path) ? path : "anon") << "': " + << base::SystemErrorCodeToString(errno); + return {}; + } + } + + incfs::IncFsFileMap file_map; + if (!file_map.Create(fd, offset, static_cast<size_t>(length), path)) { + LOG(ERROR) << "Failed to mmap file '" << ((path != nullptr) ? path : "anon") << "': " + << base::SystemErrorCodeToString(errno); + return {}; + } + + // If `path` is set, do not pass ownership of the `fd` to the new Asset since + // Asset::openFileDescriptor can use `path` to create new file descriptors. + return Asset::createFromUncompressedMap(std::move(file_map), + Asset::AccessMode::ACCESS_RANDOM, + (path != nullptr) ? base::unique_fd(-1) : std::move(fd)); +} + +ZipAssetsProvider::PathOrDebugName::PathOrDebugName(std::string&& value, bool is_path) + : value_(std::forward<std::string>(value)), is_path_(is_path) {} + +const std::string* ZipAssetsProvider::PathOrDebugName::GetPath() const { + return is_path_ ? &value_ : nullptr; +} + +const std::string& ZipAssetsProvider::PathOrDebugName::GetDebugName() const { + return value_; +} + +ZipAssetsProvider::ZipAssetsProvider(ZipArchive* handle, PathOrDebugName&& path, + time_t last_mod_time) + : zip_handle_(handle, ::CloseArchive), + name_(std::forward<PathOrDebugName>(path)), + last_mod_time_(last_mod_time) {} + +std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path) { + ZipArchiveHandle handle; + if (int32_t result = OpenArchive(path.c_str(), &handle); result != 0) { + LOG(ERROR) << "Failed to open APK '" << path << "' " << ::ErrorCodeString(result); + CloseArchive(handle); + return {}; + } + + struct stat sb{.st_mtime = -1}; + if (stat(path.c_str(), &sb) < 0) { + // Stat requires execute permissions on all directories path to the file. If the process does + // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will + // always have to return true. + LOG(WARNING) << "Failed to stat file '" << path << "': " + << base::SystemErrorCodeToString(errno); + } + + return std::unique_ptr<ZipAssetsProvider>( + new ZipAssetsProvider(handle, PathOrDebugName{std::move(path), + true /* is_path */}, sb.st_mtime)); +} + +std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd, + std::string friendly_name, + off64_t offset, + off64_t len) { + ZipArchiveHandle handle; + const int released_fd = fd.release(); + const int32_t result = (len == AssetsProvider::kUnknownLength) + ? ::OpenArchiveFd(released_fd, friendly_name.c_str(), &handle) + : ::OpenArchiveFdRange(released_fd, friendly_name.c_str(), &handle, len, offset); + + if (result != 0) { + LOG(ERROR) << "Failed to open APK '" << friendly_name << "' through FD with offset " << offset + << " and length " << len << ": " << ::ErrorCodeString(result); + CloseArchive(handle); + return {}; + } + + struct stat sb{.st_mtime = -1}; + if (fstat(released_fd, &sb) < 0) { + // Stat requires execute permissions on all directories path to the file. If the process does + // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will + // always have to return true. + LOG(WARNING) << "Failed to fstat file '" << friendly_name << "': " + << base::SystemErrorCodeToString(errno); + } + + return std::unique_ptr<ZipAssetsProvider>( + new ZipAssetsProvider(handle, PathOrDebugName{std::move(friendly_name), + false /* is_path */}, sb.st_mtime)); +} + +std::unique_ptr<Asset> ZipAssetsProvider::OpenInternal(const std::string& path, + Asset::AccessMode mode, + bool* file_exists) const { + if (file_exists != nullptr) { + *file_exists = false; + } + + ZipEntry entry; + if (FindEntry(zip_handle_.get(), path, &entry) != 0) { + return {}; + } + + if (file_exists != nullptr) { + *file_exists = true; + } + + const int fd = GetFileDescriptor(zip_handle_.get()); + const off64_t fd_offset = GetFileDescriptorOffset(zip_handle_.get()); + incfs::IncFsFileMap asset_map; + if (entry.method == kCompressDeflated) { + if (!asset_map.Create(fd, entry.offset + fd_offset, entry.compressed_length, + name_.GetDebugName().c_str())) { + LOG(ERROR) << "Failed to mmap file '" << path << "' in APK '" << name_.GetDebugName() + << "'"; + return {}; + } + + std::unique_ptr<Asset> asset = + Asset::createFromCompressedMap(std::move(asset_map), entry.uncompressed_length, mode); + if (asset == nullptr) { + LOG(ERROR) << "Failed to decompress '" << path << "' in APK '" << name_.GetDebugName() + << "'"; + return {}; + } + return asset; + } + + if (!asset_map.Create(fd, entry.offset + fd_offset, entry.uncompressed_length, + name_.GetDebugName().c_str())) { + LOG(ERROR) << "Failed to mmap file '" << path << "' in APK '" << name_.GetDebugName() << "'"; + return {}; + } + + base::unique_fd ufd; + if (name_.GetPath() == nullptr) { + // If the zip name does not represent a path, create a new `fd` for the new Asset to own in + // order to create new file descriptors using Asset::openFileDescriptor. If the zip name is a + // path, it will be used to create new file descriptors. + ufd = base::unique_fd(dup(fd)); + if (!ufd.ok()) { + LOG(ERROR) << "Unable to dup fd '" << path << "' in APK '" << name_.GetDebugName() << "'"; + return {}; + } + } + + auto asset = Asset::createFromUncompressedMap(std::move(asset_map), mode, std::move(ufd)); + if (asset == nullptr) { + LOG(ERROR) << "Failed to mmap file '" << path << "' in APK '" << name_.GetDebugName() << "'"; + return {}; + } + return asset; +} + +bool ZipAssetsProvider::ForEachFile(const std::string& root_path, + const std::function<void(const StringPiece&, FileType)>& f) + const { + std::string root_path_full = root_path; + if (root_path_full.back() != '/') { + root_path_full += '/'; + } + + void* cookie; + if (StartIteration(zip_handle_.get(), &cookie, root_path_full, "") != 0) { + return false; + } + + std::string name; + ::ZipEntry entry{}; + + // We need to hold back directories because many paths will contain them and we want to only + // surface one. + std::set<std::string> dirs{}; + + int32_t result; + while ((result = Next(cookie, &entry, &name)) == 0) { + StringPiece full_file_path(name); + StringPiece leaf_file_path = full_file_path.substr(root_path_full.size()); + + if (!leaf_file_path.empty()) { + auto iter = std::find(leaf_file_path.begin(), leaf_file_path.end(), '/'); + if (iter != leaf_file_path.end()) { + std::string dir = + leaf_file_path.substr(0, std::distance(leaf_file_path.begin(), iter)).to_string(); + dirs.insert(std::move(dir)); + } else { + f(leaf_file_path, kFileTypeRegular); + } + } + } + EndIteration(cookie); + + // Now present the unique directories. + for (const std::string& dir : dirs) { + f(dir, kFileTypeDirectory); + } + + // -1 is end of iteration, anything else is an error. + return result == -1; +} + +const std::string& ZipAssetsProvider::GetDebugName() const { + return name_.GetDebugName(); +} + +bool ZipAssetsProvider::IsUpToDate() const { + struct stat sb{}; + if (fstat(GetFileDescriptor(zip_handle_.get()), &sb) < 0) { + // If fstat fails on the zip archive, return true so the zip archive the resource system does + // attempt to refresh the ApkAsset. + return true; + } + return last_mod_time_ == sb.st_mtime; +} + +DirectoryAssetsProvider::DirectoryAssetsProvider(std::string&& path, time_t last_mod_time) + : dir_(std::forward<std::string>(path)), last_mod_time_(last_mod_time) {} + +std::unique_ptr<DirectoryAssetsProvider> DirectoryAssetsProvider::Create(std::string path) { + struct stat sb{}; + const int result = stat(path.c_str(), &sb); + if (result == -1) { + LOG(ERROR) << "Failed to find directory '" << path << "'."; + return nullptr; + } + + if (!S_ISDIR(sb.st_mode)) { + LOG(ERROR) << "Path '" << path << "' is not a directory."; + return nullptr; + } + + if (path[path.size() - 1] != OS_PATH_SEPARATOR) { + path += OS_PATH_SEPARATOR; + } + + return std::unique_ptr<DirectoryAssetsProvider>(new DirectoryAssetsProvider(std::move(path), + sb.st_mtime)); +} + +std::unique_ptr<Asset> DirectoryAssetsProvider::OpenInternal(const std::string& path, + Asset::AccessMode /* mode */, + bool* file_exists) const { + const std::string resolved_path = dir_ + path; + if (file_exists != nullptr) { + struct stat sb{}; + *file_exists = (stat(resolved_path.c_str(), &sb) != -1) && S_ISREG(sb.st_mode); + } + + return CreateAssetFromFile(resolved_path); +} + +bool DirectoryAssetsProvider::ForEachFile( + const std::string& /* root_path */, + const std::function<void(const StringPiece&, FileType)>& /* f */) + const { + return true; +} + +const std::string& DirectoryAssetsProvider::GetDebugName() const { + return dir_; +} + +bool DirectoryAssetsProvider::IsUpToDate() const { + struct stat sb{}; + if (stat(dir_.c_str(), &sb) < 0) { + // If stat fails on the zip archive, return true so the zip archive the resource system does + // attempt to refresh the ApkAsset. + return true; + } + return last_mod_time_ == sb.st_mtime; +} + +MultiAssetsProvider::MultiAssetsProvider(std::unique_ptr<AssetsProvider>&& primary, + std::unique_ptr<AssetsProvider>&& secondary) + : primary_(std::forward<std::unique_ptr<AssetsProvider>>(primary)), + secondary_(std::forward<std::unique_ptr<AssetsProvider>>(secondary)) { + if (primary_->GetDebugName() == kEmptyDebugString) { + debug_name_ = secondary_->GetDebugName(); + } else if (secondary_->GetDebugName() == kEmptyDebugString) { + debug_name_ = primary_->GetDebugName(); + } else { + debug_name_ = primary_->GetDebugName() + " and " + secondary_->GetDebugName(); + } +} + +std::unique_ptr<AssetsProvider> MultiAssetsProvider::Create( + std::unique_ptr<AssetsProvider>&& primary, std::unique_ptr<AssetsProvider>&& secondary) { + if (primary == nullptr || secondary == nullptr) { + return nullptr; + } + return std::unique_ptr<MultiAssetsProvider>(new MultiAssetsProvider(std::move(primary), + std::move(secondary))); +} + +std::unique_ptr<Asset> MultiAssetsProvider::OpenInternal(const std::string& path, + Asset::AccessMode mode, + bool* file_exists) const { + auto asset = primary_->Open(path, mode, file_exists); + return (asset) ? std::move(asset) : secondary_->Open(path, mode, file_exists); +} + +bool MultiAssetsProvider::ForEachFile(const std::string& root_path, + const std::function<void(const StringPiece&, FileType)>& f) + const { + return primary_->ForEachFile(root_path, f) && secondary_->ForEachFile(root_path, f); +} + +const std::string& MultiAssetsProvider::GetDebugName() const { + return debug_name_; +} + +bool MultiAssetsProvider::IsUpToDate() const { + return primary_->IsUpToDate() && secondary_->IsUpToDate(); +} + +std::unique_ptr<AssetsProvider> EmptyAssetsProvider::Create() { + return std::make_unique<EmptyAssetsProvider>(); +} + +std::unique_ptr<Asset> EmptyAssetsProvider::OpenInternal(const std::string& /* path */, + Asset::AccessMode /* mode */, + bool* file_exists) const { + if (file_exists) { + *file_exists = false; + } + return nullptr; +} + +bool EmptyAssetsProvider::ForEachFile( + const std::string& /* root_path */, + const std::function<void(const StringPiece&, FileType)>& /* f */) const { + return true; +} + +const std::string& EmptyAssetsProvider::GetDebugName() const { + const static std::string kEmpty = kEmptyDebugString; + return kEmpty; +} + +bool EmptyAssetsProvider::IsUpToDate() const { + return true; +} + +} // namespace android
\ No newline at end of file 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/Idmap.cpp b/libs/androidfw/Idmap.cpp index a61309514143..f216f55771c2 100644 --- a/libs/androidfw/Idmap.cpp +++ b/libs/androidfw/Idmap.cpp @@ -36,13 +36,51 @@ using ::android::base::StringPrintf; namespace android { -uint32_t round_to_4_bytes(uint32_t size) { - return size + (4U - (size % 4U)) % 4U; -} +// See frameworks/base/cmds/idmap2/include/idmap2/Idmap.h for full idmap file format specification. +struct Idmap_header { + // Always 0x504D4449 ('IDMP') + uint32_t magic; + uint32_t version; -size_t Idmap_header::Size() const { - return sizeof(Idmap_header) + sizeof(uint8_t) * round_to_4_bytes(dtohl(debug_info_size)); -} + uint32_t target_crc32; + uint32_t overlay_crc32; + + uint32_t fulfilled_policies; + uint32_t enforce_overlayable; + + // overlay_path, target_path, and other string values encoded in the idmap header and read and + // stored in separate structures. This allows the idmap header data to be casted to this struct + // without having to read/store each header entry separately. +}; + +struct Idmap_data_header { + uint8_t target_package_id; + uint8_t overlay_package_id; + + // Padding to ensure 4 byte alignment for target_entry_count + uint16_t p0; + + uint32_t target_entry_count; + uint32_t target_inline_entry_count; + uint32_t overlay_entry_count; + + uint32_t string_pool_index_offset; +}; + +struct Idmap_target_entry { + uint32_t target_id; + uint32_t overlay_id; +}; + +struct Idmap_target_entry_inline { + uint32_t target_id; + Res_value value; +}; + +struct Idmap_overlay_entry { + uint32_t overlay_id; + uint32_t target_id; +}; OverlayStringPool::OverlayStringPool(const LoadedIdmap* loaded_idmap) : data_header_(loaded_idmap->data_header_), @@ -155,153 +193,149 @@ IdmapResMap::Result IdmapResMap::Lookup(uint32_t target_res_id) const { return {}; } -static bool is_word_aligned(const void* data) { - return (reinterpret_cast<uintptr_t>(data) & 0x03U) == 0U; -} - -static bool IsValidIdmapHeader(const StringPiece& data) { - if (!is_word_aligned(data.data())) { - LOG(ERROR) << "Idmap header is not word aligned."; - return false; +namespace { +template <typename T> +const T* ReadType(const uint8_t** in_out_data_ptr, size_t* in_out_size, const std::string& label, + size_t count = 1) { + if (!util::IsFourByteAligned(*in_out_data_ptr)) { + LOG(ERROR) << "Idmap " << label << " is not word aligned."; + return {}; } - - if (data.size() < sizeof(Idmap_header)) { - LOG(ERROR) << "Idmap header is too small."; - return false; + if ((*in_out_size / sizeof(T)) < count) { + LOG(ERROR) << "Idmap too small for the number of " << label << " entries (" + << count << ")."; + return nullptr; } + auto data_ptr = *in_out_data_ptr; + const size_t read_size = sizeof(T) * count; + *in_out_data_ptr += read_size; + *in_out_size -= read_size; + return reinterpret_cast<const T*>(data_ptr); +} - auto header = reinterpret_cast<const Idmap_header*>(data.data()); - if (dtohl(header->magic) != kIdmapMagic) { - LOG(ERROR) << StringPrintf("Invalid Idmap file: bad magic value (was 0x%08x, expected 0x%08x)", - dtohl(header->magic), kIdmapMagic); - return false; +std::optional<std::string_view> ReadString(const uint8_t** in_out_data_ptr, size_t* in_out_size, + const std::string& label) { + const auto* len = ReadType<uint32_t>(in_out_data_ptr, in_out_size, label + " length"); + if (len == nullptr) { + return {}; } - - if (dtohl(header->version) != kIdmapCurrentVersion) { - // We are strict about versions because files with this format are auto-generated and don't need - // backwards compatibility. - LOG(ERROR) << StringPrintf("Version mismatch in Idmap (was 0x%08x, expected 0x%08x)", - dtohl(header->version), kIdmapCurrentVersion); - return false; + const auto* data = ReadType<char>(in_out_data_ptr, in_out_size, label, *len); + if (data == nullptr) { + return {}; } - - return true; + // Strings are padded to the next 4 byte boundary. + const uint32_t padding_size = (4U - ((size_t)*in_out_data_ptr & 0x3U)) % 4U; + for (uint32_t i = 0; i < padding_size; i++) { + if (**in_out_data_ptr != 0) { + LOG(ERROR) << " Idmap padding of " << label << " is non-zero."; + return {}; + } + *in_out_data_ptr += sizeof(uint8_t); + *in_out_size -= sizeof(uint8_t); + } + return std::string_view(data, *len); +} } LoadedIdmap::LoadedIdmap(std::string&& idmap_path, - const time_t last_mod_time, const Idmap_header* header, const Idmap_data_header* data_header, const Idmap_target_entry* target_entries, const Idmap_target_entry_inline* target_inline_entries, const Idmap_overlay_entry* overlay_entries, - ResStringPool* string_pool) + std::unique_ptr<ResStringPool>&& string_pool, + std::string_view overlay_apk_path, + std::string_view target_apk_path) : header_(header), data_header_(data_header), target_entries_(target_entries), target_inline_entries_(target_inline_entries), overlay_entries_(overlay_entries), - string_pool_(string_pool), + string_pool_(std::move(string_pool)), idmap_path_(std::move(idmap_path)), - idmap_last_mod_time_(last_mod_time) { - - size_t length = strnlen(reinterpret_cast<const char*>(header_->overlay_path), - arraysize(header_->overlay_path)); - overlay_apk_path_.assign(reinterpret_cast<const char*>(header_->overlay_path), length); + overlay_apk_path_(overlay_apk_path), + target_apk_path_(target_apk_path), + idmap_last_mod_time_(getFileModDate(idmap_path_.data())) {} - length = strnlen(reinterpret_cast<const char*>(header_->target_path), - arraysize(header_->target_path)); - target_apk_path_.assign(reinterpret_cast<const char*>(header_->target_path), length); -} - -std::unique_ptr<const LoadedIdmap> LoadedIdmap::Load(const StringPiece& idmap_path, - const StringPiece& idmap_data) { +std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(const StringPiece& idmap_path, + const StringPiece& idmap_data) { ATRACE_CALL(); - if (!IsValidIdmapHeader(idmap_data)) { + size_t data_size = idmap_data.size(); + auto data_ptr = reinterpret_cast<const uint8_t*>(idmap_data.data()); + + // Parse the idmap header + auto header = ReadType<Idmap_header>(&data_ptr, &data_size, "header"); + if (header == nullptr) { return {}; } - - auto header = reinterpret_cast<const Idmap_header*>(idmap_data.data()); - const uint8_t* data_ptr = reinterpret_cast<const uint8_t*>(idmap_data.data()) + header->Size(); - size_t data_size = idmap_data.size() - header->Size(); - - // Currently idmap2 can only generate one data block. - auto data_header = reinterpret_cast<const Idmap_data_header*>(data_ptr); - data_ptr += sizeof(*data_header); - data_size -= sizeof(*data_header); - - // Make sure there is enough space for the target entries declared in the header - const auto target_entries = reinterpret_cast<const Idmap_target_entry*>(data_ptr); - if (data_size / sizeof(Idmap_target_entry) < - static_cast<size_t>(dtohl(data_header->target_entry_count))) { - LOG(ERROR) << StringPrintf("Idmap too small for the number of target entries (%d)", - (int)dtohl(data_header->target_entry_count)); + if (dtohl(header->magic) != kIdmapMagic) { + LOG(ERROR) << StringPrintf("Invalid Idmap file: bad magic value (was 0x%08x, expected 0x%08x)", + dtohl(header->magic), kIdmapMagic); return {}; } - - // Advance the data pointer past the target entries. - const size_t target_entry_size_bytes = - (dtohl(data_header->target_entry_count) * sizeof(Idmap_target_entry)); - data_ptr += target_entry_size_bytes; - data_size -= target_entry_size_bytes; - - // Make sure there is enough space for the target entries declared in the header. - const auto target_inline_entries = reinterpret_cast<const Idmap_target_entry_inline*>(data_ptr); - if (data_size / sizeof(Idmap_target_entry_inline) < - static_cast<size_t>(dtohl(data_header->target_inline_entry_count))) { - LOG(ERROR) << StringPrintf("Idmap too small for the number of target inline entries (%d)", - (int)dtohl(data_header->target_inline_entry_count)); + if (dtohl(header->version) != kIdmapCurrentVersion) { + // We are strict about versions because files with this format are generated at runtime and + // don't need backwards compatibility. + LOG(ERROR) << StringPrintf("Version mismatch in Idmap (was 0x%08x, expected 0x%08x)", + dtohl(header->version), kIdmapCurrentVersion); return {}; } - - // Advance the data pointer past the target entries. - const size_t target_inline_entry_size_bytes = - (dtohl(data_header->target_inline_entry_count) * sizeof(Idmap_target_entry_inline)); - data_ptr += target_inline_entry_size_bytes; - data_size -= target_inline_entry_size_bytes; - - // Make sure there is enough space for the overlay entries declared in the header. - const auto overlay_entries = reinterpret_cast<const Idmap_overlay_entry*>(data_ptr); - if (data_size / sizeof(Idmap_overlay_entry) < - static_cast<size_t>(dtohl(data_header->overlay_entry_count))) { - LOG(ERROR) << StringPrintf("Idmap too small for the number of overlay entries (%d)", - (int)dtohl(data_header->overlay_entry_count)); + std::optional<std::string_view> overlay_path = ReadString(&data_ptr, &data_size, "overlay path"); + if (!overlay_path) { return {}; } - - // Advance the data pointer past the overlay entries. - const size_t overlay_entry_size_bytes = - (dtohl(data_header->overlay_entry_count) * sizeof(Idmap_overlay_entry)); - data_ptr += overlay_entry_size_bytes; - data_size -= overlay_entry_size_bytes; - - // Read the idmap string pool that holds the value of inline string entries. - uint32_t string_pool_size = dtohl(*reinterpret_cast<const uint32_t*>(data_ptr)); - data_ptr += sizeof(uint32_t); - data_size -= sizeof(uint32_t); - - if (data_size < string_pool_size) { - LOG(ERROR) << StringPrintf("Idmap too small for string pool (length %d)", - (int)string_pool_size); + std::optional<std::string_view> target_path = ReadString(&data_ptr, &data_size, "target path"); + if (!target_path) { + return {}; + } + if (!ReadString(&data_ptr, &data_size, "target name") || + !ReadString(&data_ptr, &data_size, "debug info")) { return {}; } + // Parse the idmap data blocks. Currently idmap2 can only generate one data block. + auto data_header = ReadType<Idmap_data_header>(&data_ptr, &data_size, "data header"); + if (data_header == nullptr) { + return {}; + } + auto target_entries = ReadType<Idmap_target_entry>(&data_ptr, &data_size, "target", + dtohl(data_header->target_entry_count)); + if (target_entries == nullptr) { + return {}; + } + auto target_inline_entries = ReadType<Idmap_target_entry_inline>( + &data_ptr, &data_size, "target inline", dtohl(data_header->target_inline_entry_count)); + if (target_inline_entries == nullptr) { + return {}; + } + auto overlay_entries = ReadType<Idmap_overlay_entry>(&data_ptr, &data_size, "target inline", + dtohl(data_header->overlay_entry_count)); + if (overlay_entries == nullptr) { + return {}; + } + std::optional<std::string_view> string_pool = ReadString(&data_ptr, &data_size, "string pool"); + if (!string_pool) { + return {}; + } auto idmap_string_pool = util::make_unique<ResStringPool>(); - if (string_pool_size > 0) { - status_t err = idmap_string_pool->setTo(data_ptr, string_pool_size); + if (!string_pool->empty()) { + const status_t err = idmap_string_pool->setTo(string_pool->data(), string_pool->size()); if (err != NO_ERROR) { LOG(ERROR) << "idmap string pool corrupt."; return {}; } } - // Can't use make_unique because LoadedIdmap constructor is private. - auto loaded_idmap = std::unique_ptr<LoadedIdmap>( - new LoadedIdmap(idmap_path.to_string(), getFileModDate(idmap_path.data()), header, - data_header, target_entries, target_inline_entries, overlay_entries, - idmap_string_pool.release())); + if (data_size != 0) { + LOG(ERROR) << "idmap parsed with " << data_size << "bytes remaining"; + return {}; + } - return std::move(loaded_idmap); + // Can't use make_unique because LoadedIdmap constructor is private. + return std::unique_ptr<LoadedIdmap>( + new LoadedIdmap(idmap_path.to_string(), header, data_header, target_entries, + target_inline_entries, overlay_entries, std::move(idmap_string_pool), + *target_path, *overlay_path)); } bool LoadedIdmap::IsUpToDate() const { diff --git a/libs/androidfw/LoadedArsc.cpp b/libs/androidfw/LoadedArsc.cpp index 2fc3b05011c2..2a70f0d6a804 100644 --- a/libs/androidfw/LoadedArsc.cpp +++ b/libs/androidfw/LoadedArsc.cpp @@ -33,7 +33,6 @@ #endif #endif -#include "androidfw/ByteBucketArray.h" #include "androidfw/Chunk.h" #include "androidfw/ResourceUtils.h" #include "androidfw/Util.h" @@ -49,36 +48,24 @@ namespace { // Builder that helps accumulate Type structs and then create a single // contiguous block of memory to store both the TypeSpec struct and // the Type structs. -class TypeSpecPtrBuilder { - public: - explicit TypeSpecPtrBuilder(incfs::verified_map_ptr<ResTable_typeSpec> header) - : header_(header) { - } +struct TypeSpecBuilder { + explicit TypeSpecBuilder(incfs::verified_map_ptr<ResTable_typeSpec> header) : header_(header) {} void AddType(incfs::verified_map_ptr<ResTable_type> type) { - types_.push_back(type); + TypeSpec::TypeEntry& entry = type_entries.emplace_back(); + entry.config.copyFromDtoH(type->config); + entry.type = type; } - TypeSpecPtr Build() { - // Check for overflow. - using ElementType = incfs::verified_map_ptr<ResTable_type>; - if ((std::numeric_limits<size_t>::max() - sizeof(TypeSpec)) / sizeof(ElementType) < - types_.size()) { - return {}; - } - TypeSpec* type_spec = - (TypeSpec*)::malloc(sizeof(TypeSpec) + (types_.size() * sizeof(ElementType))); - type_spec->type_spec = header_; - type_spec->type_count = types_.size(); - memcpy(type_spec + 1, types_.data(), types_.size() * sizeof(ElementType)); - return TypeSpecPtr(type_spec); + TypeSpec Build() { + return {header_, std::move(type_entries)}; } private: - DISALLOW_COPY_AND_ASSIGN(TypeSpecPtrBuilder); + DISALLOW_COPY_AND_ASSIGN(TypeSpecBuilder); incfs::verified_map_ptr<ResTable_typeSpec> header_; - std::vector<incfs::verified_map_ptr<ResTable_type>> types_; + std::vector<TypeSpec::TypeEntry> type_entries; }; } // namespace @@ -322,15 +309,10 @@ base::expected<incfs::map_ptr<ResTable_entry>, NullOrIOError> LoadedPackage::Get } base::expected<std::monostate, IOError> LoadedPackage::CollectConfigurations( - bool exclude_mipmap, std::set<ResTable_config>* out_configs) const { - const size_t type_count = type_specs_.size(); - for (size_t i = 0; i < type_count; i++) { - const TypeSpecPtr& type_spec = type_specs_[i]; - if (type_spec == nullptr) { - continue; - } + bool exclude_mipmap, std::set<ResTable_config>* out_configs) const {\ + for (const auto& type_spec : type_specs_) { if (exclude_mipmap) { - const int type_idx = type_spec->type_spec->id - 1; + const int type_idx = type_spec.first - 1; const auto type_name16 = type_string_pool_.stringAt(type_idx); if (UNLIKELY(IsIOError(type_name16))) { return base::unexpected(GetIOError(type_name16.error())); @@ -354,11 +336,8 @@ base::expected<std::monostate, IOError> LoadedPackage::CollectConfigurations( } } - const auto iter_end = type_spec->types + type_spec->type_count; - for (auto iter = type_spec->types; iter != iter_end; ++iter) { - ResTable_config config; - config.copyFromDtoH((*iter)->config); - out_configs->insert(config); + for (const auto& type_entry : type_spec.second.type_entries) { + out_configs->insert(type_entry.config); } } return {}; @@ -366,19 +345,12 @@ base::expected<std::monostate, IOError> LoadedPackage::CollectConfigurations( void LoadedPackage::CollectLocales(bool canonicalize, std::set<std::string>* out_locales) const { char temp_locale[RESTABLE_MAX_LOCALE_LEN]; - const size_t type_count = type_specs_.size(); - for (size_t i = 0; i < type_count; i++) { - const TypeSpecPtr& type_spec = type_specs_[i]; - if (type_spec != nullptr) { - const auto iter_end = type_spec->types + type_spec->type_count; - for (auto iter = type_spec->types; iter != iter_end; ++iter) { - ResTable_config configuration; - configuration.copyFromDtoH((*iter)->config); - if (configuration.locale != 0) { - configuration.getBcp47Locale(temp_locale, canonicalize); - std::string locale(temp_locale); - out_locales->insert(std::move(locale)); - } + for (const auto& type_spec : type_specs_) { + for (const auto& type_entry : type_spec.second.type_entries) { + if (type_entry.config.locale != 0) { + type_entry.config.getBcp47Locale(temp_locale, canonicalize); + std::string locale(temp_locale); + out_locales->insert(std::move(locale)); } } } @@ -398,14 +370,13 @@ base::expected<uint32_t, NullOrIOError> LoadedPackage::FindEntryByName( return base::unexpected(key_idx.error()); } - const TypeSpec* type_spec = type_specs_[*type_idx].get(); + const TypeSpec* type_spec = GetTypeSpecByTypeIndex(*type_idx); if (type_spec == nullptr) { return base::unexpected(std::nullopt); } - const auto iter_end = type_spec->types + type_spec->type_count; - for (auto iter = type_spec->types; iter != iter_end; ++iter) { - const incfs::verified_map_ptr<ResTable_type>& type = *iter; + for (const auto& type_entry : type_spec->type_entries) { + const incfs::verified_map_ptr<ResTable_type>& type = type_entry.type; size_t entry_count = dtohl(type->entryCount); for (size_t entry_idx = 0; entry_idx < entry_count; entry_idx++) { @@ -492,7 +463,7 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, // A map of TypeSpec builders, each associated with an type index. // We use these to accumulate the set of Types available for a TypeSpec, and later build a single, // contiguous block of memory that holds all the Types together with the TypeSpec. - std::unordered_map<int, std::unique_ptr<TypeSpecPtrBuilder>> type_builder_map; + std::unordered_map<int, std::unique_ptr<TypeSpecBuilder>> type_builder_map; ChunkIterator iter(chunk.data_ptr(), chunk.data_size()); while (iter.HasNext()) { @@ -562,9 +533,9 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, return {}; } - std::unique_ptr<TypeSpecPtrBuilder>& builder_ptr = type_builder_map[type_spec->id - 1]; + std::unique_ptr<TypeSpecBuilder>& builder_ptr = type_builder_map[type_spec->id]; if (builder_ptr == nullptr) { - builder_ptr = util::make_unique<TypeSpecPtrBuilder>(type_spec.verified()); + builder_ptr = util::make_unique<TypeSpecBuilder>(type_spec.verified()); loaded_package->resource_ids_.set(type_spec->id, entry_count); } else { LOG(WARNING) << StringPrintf("RES_TABLE_TYPE_SPEC_TYPE already defined for ID %02x", @@ -584,7 +555,7 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, } // Type chunks must be preceded by their TypeSpec chunks. - std::unique_ptr<TypeSpecPtrBuilder>& builder_ptr = type_builder_map[type->id - 1]; + std::unique_ptr<TypeSpecBuilder>& builder_ptr = type_builder_map[type->id]; if (builder_ptr != nullptr) { builder_ptr->AddType(type.verified()); } else { @@ -722,14 +693,9 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, // Flatten and construct the TypeSpecs. for (auto& entry : type_builder_map) { - uint8_t type_idx = static_cast<uint8_t>(entry.first); - TypeSpecPtr type_spec_ptr = entry.second->Build(); - if (type_spec_ptr == nullptr) { - LOG(ERROR) << "Too many type configurations, overflow detected."; - return {}; - } - - loaded_package->type_specs_.editItemAt(type_idx) = std::move(type_spec_ptr); + TypeSpec type_spec = entry.second->Build(); + uint8_t type_id = static_cast<uint8_t>(entry.first); + loaded_package->type_specs_[type_id] = std::move(type_spec); } return std::move(loaded_package); @@ -801,10 +767,10 @@ bool LoadedArsc::LoadTable(const Chunk& chunk, const LoadedIdmap* loaded_idmap, return true; } -std::unique_ptr<const LoadedArsc> LoadedArsc::Load(incfs::map_ptr<void> data, - const size_t length, - const LoadedIdmap* loaded_idmap, - const package_property_t property_flags) { +std::unique_ptr<LoadedArsc> LoadedArsc::Load(incfs::map_ptr<void> data, + const size_t length, + const LoadedIdmap* loaded_idmap, + const package_property_t property_flags) { ATRACE_NAME("LoadedArsc::Load"); // Not using make_unique because the constructor is private. @@ -833,11 +799,10 @@ std::unique_ptr<const LoadedArsc> LoadedArsc::Load(incfs::map_ptr<void> data, } } - // Need to force a move for mingw32. - return std::move(loaded_arsc); + return loaded_arsc; } -std::unique_ptr<const LoadedArsc> LoadedArsc::CreateEmpty() { +std::unique_ptr<LoadedArsc> LoadedArsc::CreateEmpty() { return std::unique_ptr<LoadedArsc>(new LoadedArsc()); } diff --git a/libs/androidfw/fuzz/cursorwindow_fuzzer/Android.bp b/libs/androidfw/fuzz/cursorwindow_fuzzer/Android.bp new file mode 100644 index 000000000000..b36ff0968ba3 --- /dev/null +++ b/libs/androidfw/fuzz/cursorwindow_fuzzer/Android.bp @@ -0,0 +1,35 @@ +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", + ], + }, + darwin: { + // libbinder is not supported on mac + enabled: false, + }, + }, +} 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/ApkAssets.h b/libs/androidfw/include/androidfw/ApkAssets.h index e57490aab2d8..d0019ed6be30 100644 --- a/libs/androidfw/include/androidfw/ApkAssets.h +++ b/libs/androidfw/include/androidfw/ApkAssets.h @@ -24,104 +24,49 @@ #include "android-base/unique_fd.h" #include "androidfw/Asset.h" +#include "androidfw/AssetsProvider.h" #include "androidfw/Idmap.h" #include "androidfw/LoadedArsc.h" #include "androidfw/misc.h" -struct ZipArchive; -typedef ZipArchive* ZipArchiveHandle; - namespace android { -class LoadedIdmap; - -// Interface for retrieving assets provided by an ApkAssets. -class AssetsProvider { +// Holds an APK. +class ApkAssets { public: - virtual ~AssetsProvider() = default; - // Opens a file for reading. - std::unique_ptr<Asset> Open(const std::string& path, - Asset::AccessMode mode = Asset::AccessMode::ACCESS_RANDOM, - bool* file_exists = nullptr) const { - return OpenInternal(path, mode, file_exists); - } + // Creates an ApkAssets from a path on device. + static std::unique_ptr<ApkAssets> Load(const std::string& path, + package_property_t flags = 0U); - // Iterate over all files and directories provided by the zip. The order of iteration is stable. - virtual bool ForEachFile(const std::string& /* path */, - const std::function<void(const StringPiece&, FileType)>& /* f */) const { - return true; - } + // Creates an ApkAssets from an open file descriptor. + static std::unique_ptr<ApkAssets> LoadFromFd(base::unique_fd fd, + const std::string& debug_name, + package_property_t flags = 0U, + off64_t offset = 0, + off64_t len = AssetsProvider::kUnknownLength); - protected: - AssetsProvider() = default; + // Creates an ApkAssets from an AssetProvider. + // The ApkAssets will take care of destroying the AssetsProvider when it is destroyed. + static std::unique_ptr<ApkAssets> Load(std::unique_ptr<AssetsProvider> assets, + package_property_t flags = 0U); - virtual std::unique_ptr<Asset> OpenInternal(const std::string& path, - Asset::AccessMode mode, - bool* file_exists) const = 0; - - private: - DISALLOW_COPY_AND_ASSIGN(AssetsProvider); -}; - -class ZipAssetsProvider; - -// Holds an APK. -class ApkAssets { - public: - // This means the data extends to the end of the file. - static constexpr off64_t kUnknownLength = -1; - - // Creates an ApkAssets. - // If `system` is true, the package is marked as a system package, and allows some functions to - // filter out this package when computing what configurations/resources are available. - static std::unique_ptr<const ApkAssets> Load( - const std::string& path, package_property_t flags = 0U, - std::unique_ptr<const AssetsProvider> override_asset = nullptr); - - // Creates an ApkAssets from the given file descriptor, and takes ownership of the file - // descriptor. The `friendly_name` is some name that will be used to identify the source of - // this ApkAssets in log messages and other debug scenarios. - // If `length` equals kUnknownLength, offset must equal 0; otherwise, the apk data will be read - // using the `offset` into the file descriptor and will be `length` bytes long. - static std::unique_ptr<const ApkAssets> LoadFromFd( - base::unique_fd fd, const std::string& friendly_name, package_property_t flags = 0U, - std::unique_ptr<const AssetsProvider> override_asset = nullptr, off64_t offset = 0, - off64_t length = kUnknownLength); - - // Creates an ApkAssets from the given path which points to a resources.arsc. - static std::unique_ptr<const ApkAssets> LoadTable( - const std::string& path, package_property_t flags = 0U, - std::unique_ptr<const AssetsProvider> override_asset = nullptr); - - // Creates an ApkAssets from the given file descriptor which points to an resources.arsc, and - // takes ownership of the file descriptor. - // If `length` equals kUnknownLength, offset must equal 0; otherwise, the .arsc data will be read - // using the `offset` into the file descriptor and will be `length` bytes long. - static std::unique_ptr<const ApkAssets> LoadTableFromFd( - base::unique_fd fd, const std::string& friendly_name, package_property_t flags = 0U, - std::unique_ptr<const AssetsProvider> override_asset = nullptr, off64_t offset = 0, - off64_t length = kUnknownLength); + // Creates an ApkAssets from the given asset file representing a resources.arsc. + static std::unique_ptr<ApkAssets> LoadTable(std::unique_ptr<Asset> resources_asset, + std::unique_ptr<AssetsProvider> assets, + package_property_t flags = 0U); // Creates an ApkAssets from an IDMAP, which contains the original APK path, and the overlay // data. - static std::unique_ptr<const ApkAssets> LoadOverlay(const std::string& idmap_path, - package_property_t flags = 0U); - - // Creates an ApkAssets from the directory path. File-based resources are read within the - // directory as if the directory is an APK. - static std::unique_ptr<const ApkAssets> LoadFromDir( - const std::string& path, package_property_t flags = 0U, - std::unique_ptr<const AssetsProvider> override_asset = nullptr); - - // Creates a totally empty ApkAssets with no resources table and no file entries. - static std::unique_ptr<const ApkAssets> LoadEmpty( - package_property_t flags = 0U, - std::unique_ptr<const AssetsProvider> override_asset = nullptr); - - const std::string& GetPath() const { - return path_; - } + static std::unique_ptr<ApkAssets> LoadOverlay(const std::string& idmap_path, + package_property_t flags = 0U); + + // TODO(177101983): Remove all uses of GetPath for checking whether two ApkAssets are the same. + // With the introduction of ResourcesProviders, not all ApkAssets have paths. This could cause + // bugs when path is used for comparison because multiple ApkAssets could have the same "firendly + // name". Use pointer equality instead. ResourceManager caches and reuses ApkAssets so the + // same asset should have the same pointer. + const std::string& GetPath() const; const AssetsProvider* GetAssetsProvider() const { return assets_provider_.get(); @@ -146,53 +91,40 @@ class ApkAssets { // Returns whether the resources.arsc is allocated in RAM (not mmapped). bool IsTableAllocated() const { - return resources_asset_ && resources_asset_->isAllocated(); + return resources_asset_ != nullptr && resources_asset_->isAllocated(); } bool IsUpToDate() const; - // Creates an Asset from a file on disk. - static std::unique_ptr<Asset> CreateAssetFromFile(const std::string& path); - - // Creates an Asset from a file descriptor. - // - // The asset takes ownership of the file descriptor. If `length` equals kUnknownLength, offset - // must equal 0; otherwise, the asset data will be read using the `offset` into the file - // descriptor and will be `length` bytes long. - static std::unique_ptr<Asset> CreateAssetFromFd(base::unique_fd fd, - const char* path, - off64_t offset = 0, - off64_t length = kUnknownLength); private: - DISALLOW_COPY_AND_ASSIGN(ApkAssets); - - static std::unique_ptr<const ApkAssets> LoadImpl( - std::unique_ptr<const AssetsProvider> assets, const std::string& path, - package_property_t property_flags, - std::unique_ptr<const AssetsProvider> override_assets = nullptr, - std::unique_ptr<Asset> idmap_asset = nullptr, - std::unique_ptr<const LoadedIdmap> idmap = nullptr); - - static std::unique_ptr<const ApkAssets> LoadTableImpl( - std::unique_ptr<Asset> resources_asset, const std::string& path, - package_property_t property_flags, - std::unique_ptr<const AssetsProvider> override_assets = nullptr); - - ApkAssets(std::unique_ptr<const AssetsProvider> assets_provider, - std::string path, - time_t last_mod_time, - package_property_t property_flags); - - std::unique_ptr<const AssetsProvider> assets_provider_; - const std::string path_; - time_t last_mod_time_; - package_property_t property_flags_ = 0U; + static std::unique_ptr<ApkAssets> LoadImpl(std::unique_ptr<AssetsProvider> assets, + package_property_t property_flags, + std::unique_ptr<Asset> idmap_asset, + std::unique_ptr<LoadedIdmap> loaded_idmap); + + static std::unique_ptr<ApkAssets> LoadImpl(std::unique_ptr<Asset> resources_asset, + std::unique_ptr<AssetsProvider> assets, + package_property_t property_flags, + std::unique_ptr<Asset> idmap_asset, + std::unique_ptr<LoadedIdmap> loaded_idmap); + + ApkAssets(std::unique_ptr<Asset> resources_asset, + std::unique_ptr<LoadedArsc> loaded_arsc, + std::unique_ptr<AssetsProvider> assets, + package_property_t property_flags, + std::unique_ptr<Asset> idmap_asset, + std::unique_ptr<LoadedIdmap> loaded_idmap); + std::unique_ptr<Asset> resources_asset_; + std::unique_ptr<LoadedArsc> loaded_arsc_; + + std::unique_ptr<AssetsProvider> assets_provider_; + package_property_t property_flags_ = 0U; + std::unique_ptr<Asset> idmap_asset_; - std::unique_ptr<const LoadedArsc> loaded_arsc_; - std::unique_ptr<const LoadedIdmap> loaded_idmap_; + std::unique_ptr<LoadedIdmap> loaded_idmap_; }; -} // namespace android +} // namespace android -#endif /* APKASSETS_H_ */ +#endif // APKASSETS_H_
\ No newline at end of file diff --git a/libs/androidfw/include/androidfw/Asset.h b/libs/androidfw/include/androidfw/Asset.h index 80bae20f3419..40c91a6fcbf5 100644 --- a/libs/androidfw/include/androidfw/Asset.h +++ b/libs/androidfw/include/androidfw/Asset.h @@ -167,8 +167,8 @@ protected: private: /* AssetManager needs access to our "create" functions */ friend class AssetManager; - friend class ApkAssets; - friend class ZipAssetsProvider; + friend struct ZipAssetsProvider; + friend struct AssetsProvider; /* * Create the asset from a named file on disk. diff --git a/libs/androidfw/include/androidfw/AssetManager2.h b/libs/androidfw/include/androidfw/AssetManager2.h index a92694c94b9f..6fbd6aa0df7b 100644 --- a/libs/androidfw/include/androidfw/AssetManager2.h +++ b/libs/androidfw/include/androidfw/AssetManager2.h @@ -101,12 +101,7 @@ class AssetManager2 { // Only pass invalidate_caches=false when it is known that the structure // change in ApkAssets is due to a safe addition of resources with completely // new resource IDs. - // - // Only pass in filter_incompatible_configs=false when you want to load all - // configurations (including incompatible ones) such as when constructing an - // idmap. - bool SetApkAssets(const std::vector<const ApkAssets*>& apk_assets, bool invalidate_caches = true, - bool filter_incompatible_configs = true); + bool SetApkAssets(const std::vector<const ApkAssets*>& apk_assets, bool invalidate_caches = true); inline const std::vector<const ApkAssets*> GetApkAssets() const { return apk_assets_; @@ -298,6 +293,12 @@ class AssetManager2 { // data failed. base::expected<const ResolvedBag*, NullOrIOError> ResolveBag(SelectedValue& value) const; + // Returns the android::ResTable_typeSpec flags of the resource ID. + // + // Returns a null error if the resource could not be resolved, or an I/O error if reading + // resource data failed. + base::expected<uint32_t, NullOrIOError> GetResourceTypeSpecFlags(uint32_t resid) const; + const std::vector<uint32_t> GetBagResIdStack(uint32_t resid) const; // Resets the resource resolution structures in preparation for the next resource retrieval. @@ -330,15 +331,10 @@ class AssetManager2 { private: DISALLOW_COPY_AND_ASSIGN(AssetManager2); - struct TypeConfig { - incfs::verified_map_ptr<ResTable_type> type; - ResTable_config config; - }; - // A collection of configurations and their associated ResTable_type that match the current // AssetManager configuration. struct FilteredConfigGroup { - std::vector<TypeConfig> type_configs; + std::vector<const TypeSpec::TypeEntry*> type_entries; }; // Represents an single package. @@ -413,7 +409,7 @@ class AssetManager2 { // Triggers the re-construction of lists of types that match the set configuration. // This should always be called when mutating the AssetManager's configuration or ApkAssets set. - void RebuildFilterList(bool filter_incompatible_configs = true); + void RebuildFilterList(); // Retrieves the APK paths of overlays that overlay non-system packages. std::set<std::string> GetNonSystemOverlayPaths() const; @@ -460,13 +456,9 @@ class AssetManager2 { enum class Type { INITIAL, BETTER_MATCH, - BETTER_MATCH_LOADER, OVERLAID, - OVERLAID_LOADER, SKIPPED, - SKIPPED_LOADER, NO_ENTRY, - NO_ENTRY_LOADER, }; // Marks what kind of override this step was. @@ -477,6 +469,9 @@ class AssetManager2 { // Marks the package name of the better resource found in this step. const std::string* package_name; + + // + ApkAssetsCookie cookie = kInvalidCookie; }; // Last resolved resource ID. diff --git a/libs/androidfw/include/androidfw/AssetsProvider.h b/libs/androidfw/include/androidfw/AssetsProvider.h new file mode 100644 index 000000000000..7b06947f45aa --- /dev/null +++ b/libs/androidfw/include/androidfw/AssetsProvider.h @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROIDFW_ASSETSPROVIDER_H +#define ANDROIDFW_ASSETSPROVIDER_H + +#include <memory> +#include <string> + +#include "android-base/macros.h" +#include "android-base/unique_fd.h" + +#include "androidfw/Asset.h" +#include "androidfw/Idmap.h" +#include "androidfw/LoadedArsc.h" +#include "androidfw/misc.h" + +struct ZipArchive; + +namespace android { + +// Interface responsible for opening and iterating through asset files. +struct AssetsProvider { + static constexpr off64_t kUnknownLength = -1; + + // Opens a file for reading. If `file_exists` is not null, it will be set to `true` if the file + // exists. This is useful for determining if the file exists but was unable to be opened due to + // an I/O error. + std::unique_ptr<Asset> Open(const std::string& path, + Asset::AccessMode mode = Asset::AccessMode::ACCESS_RANDOM, + bool* file_exists = nullptr) const; + + // Iterate over all files and directories provided by the interface. The order of iteration is + // stable. + virtual bool ForEachFile(const std::string& path, + const std::function<void(const StringPiece&, FileType)>& f) const = 0; + + // Retrieves a name that represents the interface. This may or may not be the path of the + // interface source. + WARN_UNUSED virtual const std::string& GetDebugName() const = 0; + + // Returns whether the interface provides the most recent version of its files. + WARN_UNUSED virtual bool IsUpToDate() const = 0; + + // Creates an Asset from a file on disk. + static std::unique_ptr<Asset> CreateAssetFromFile(const std::string& path); + + // Creates an Asset from a file descriptor. + // + // The asset takes ownership of the file descriptor. If `length` equals kUnknownLength, offset + // must equal 0; otherwise, the asset data will be read using the `offset` into the file + // descriptor and will be `length` bytes long. + static std::unique_ptr<Asset> CreateAssetFromFd(base::unique_fd fd, + const char* path, + off64_t offset = 0, + off64_t length = AssetsProvider::kUnknownLength); + + virtual ~AssetsProvider() = default; + protected: + virtual std::unique_ptr<Asset> OpenInternal(const std::string& path, Asset::AccessMode mode, + bool* file_exists) const = 0; +}; + +// Supplies assets from a zip archive. +struct ZipAssetsProvider : public AssetsProvider { + static std::unique_ptr<ZipAssetsProvider> Create(std::string path); + static std::unique_ptr<ZipAssetsProvider> Create(base::unique_fd fd, + std::string friendly_name, + off64_t offset = 0, + off64_t len = kUnknownLength); + + bool ForEachFile(const std::string& root_path, + const std::function<void(const StringPiece&, FileType)>& f) const override; + + WARN_UNUSED const std::string& GetDebugName() const override; + WARN_UNUSED bool IsUpToDate() const override; + + ~ZipAssetsProvider() override = default; + protected: + std::unique_ptr<Asset> OpenInternal(const std::string& path, Asset::AccessMode mode, + bool* file_exists) const override; + + private: + struct PathOrDebugName; + ZipAssetsProvider(ZipArchive* handle, PathOrDebugName&& path, time_t last_mod_time); + + struct PathOrDebugName { + PathOrDebugName(std::string&& value, bool is_path); + + // Retrieves the path or null if this class represents a debug name. + WARN_UNUSED const std::string* GetPath() const; + + // Retrieves a name that represents the interface. This may or may not represent a path. + WARN_UNUSED const std::string& GetDebugName() const; + + private: + std::string value_; + bool is_path_; + }; + + std::unique_ptr<ZipArchive, void (*)(ZipArchive*)> zip_handle_; + PathOrDebugName name_; + time_t last_mod_time_; +}; + +// Supplies assets from a root directory. +struct DirectoryAssetsProvider : public AssetsProvider { + static std::unique_ptr<DirectoryAssetsProvider> Create(std::string root_dir); + + bool ForEachFile(const std::string& path, + const std::function<void(const StringPiece&, FileType)>& f) const override; + + WARN_UNUSED const std::string& GetDebugName() const override; + WARN_UNUSED bool IsUpToDate() const override; + + ~DirectoryAssetsProvider() override = default; + protected: + std::unique_ptr<Asset> OpenInternal(const std::string& path, + Asset::AccessMode mode, + bool* file_exists) const override; + + private: + explicit DirectoryAssetsProvider(std::string&& path, time_t last_mod_time); + std::string dir_; + time_t last_mod_time_; +}; + +// Supplies assets from a `primary` asset provider and falls back to supplying assets from the +// `secondary` asset provider if the asset cannot be found in the `primary`. +struct MultiAssetsProvider : public AssetsProvider { + static std::unique_ptr<AssetsProvider> Create(std::unique_ptr<AssetsProvider>&& primary, + std::unique_ptr<AssetsProvider>&& secondary); + + bool ForEachFile(const std::string& root_path, + const std::function<void(const StringPiece&, FileType)>& f) const override; + + WARN_UNUSED const std::string& GetDebugName() const override; + WARN_UNUSED bool IsUpToDate() const override; + + ~MultiAssetsProvider() override = default; + protected: + std::unique_ptr<Asset> OpenInternal( + const std::string& path, Asset::AccessMode mode, bool* file_exists) const override; + + private: + MultiAssetsProvider(std::unique_ptr<AssetsProvider>&& primary, + std::unique_ptr<AssetsProvider>&& secondary); + + std::unique_ptr<AssetsProvider> primary_; + std::unique_ptr<AssetsProvider> secondary_; + std::string debug_name_; +}; + +// Does not provide any assets. +struct EmptyAssetsProvider : public AssetsProvider { + static std::unique_ptr<AssetsProvider> Create(); + + bool ForEachFile(const std::string& path, + const std::function<void(const StringPiece&, FileType)>& f) const override; + + WARN_UNUSED const std::string& GetDebugName() const override; + WARN_UNUSED bool IsUpToDate() const override; + + ~EmptyAssetsProvider() override = default; + protected: + std::unique_ptr<Asset> OpenInternal(const std::string& path, Asset::AccessMode mode, + bool* file_exists) const override; +}; + +} // namespace android + +#endif /* ANDROIDFW_ASSETSPROVIDER_H */ 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/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h index fdab03ba2de4..0ded79309bc1 100644 --- a/libs/androidfw/include/androidfw/Idmap.h +++ b/libs/androidfw/include/androidfw/Idmap.h @@ -31,6 +31,11 @@ namespace android { class LoadedIdmap; class IdmapResMap; +struct Idmap_header; +struct Idmap_data_header; +struct Idmap_target_entry; +struct Idmap_target_entry_inline; +struct Idmap_overlay_entry; // A string pool for overlay apk assets. The string pool holds the strings of the overlay resources // table and additionally allows for loading strings from the idmap string pool. The idmap string @@ -144,33 +149,33 @@ class IdmapResMap { class LoadedIdmap { public: // Loads an IDMAP from a chunk of memory. Returns nullptr if the IDMAP data was malformed. - static std::unique_ptr<const LoadedIdmap> Load(const StringPiece& idmap_path, - const StringPiece& idmap_data); + static std::unique_ptr<LoadedIdmap> Load(const StringPiece& idmap_path, + const StringPiece& idmap_data); // Returns the path to the IDMAP. - inline const std::string& IdmapPath() const { + std::string_view IdmapPath() const { return idmap_path_; } // Returns the path to the RRO (Runtime Resource Overlay) APK for which this IDMAP was generated. - inline const std::string& OverlayApkPath() const { + std::string_view OverlayApkPath() const { return overlay_apk_path_; } // Returns the path to the RRO (Runtime Resource Overlay) APK for which this IDMAP was generated. - inline const std::string& TargetApkPath() const { + std::string_view TargetApkPath() const { return target_apk_path_; } // Returns a mapping from target resource ids to overlay values. - inline const IdmapResMap GetTargetResourcesMap( + const IdmapResMap GetTargetResourcesMap( uint8_t target_assigned_package_id, const OverlayDynamicRefTable* overlay_ref_table) const { return IdmapResMap(data_header_, target_entries_, target_inline_entries_, target_assigned_package_id, overlay_ref_table); } // Returns a dynamic reference table for a loaded overlay package. - inline const OverlayDynamicRefTable GetOverlayDynamicRefTable( + const OverlayDynamicRefTable GetOverlayDynamicRefTable( uint8_t target_assigned_package_id) const { return OverlayDynamicRefTable(data_header_, overlay_entries_, target_assigned_package_id); } @@ -190,22 +195,23 @@ class LoadedIdmap { const Idmap_overlay_entry* overlay_entries_; const std::unique_ptr<ResStringPool> string_pool_; - const std::string idmap_path_; - std::string overlay_apk_path_; - std::string target_apk_path_; - const time_t idmap_last_mod_time_; + std::string idmap_path_; + std::string_view overlay_apk_path_; + std::string_view target_apk_path_; + time_t idmap_last_mod_time_; private: DISALLOW_COPY_AND_ASSIGN(LoadedIdmap); explicit LoadedIdmap(std::string&& idmap_path, - time_t last_mod_time, const Idmap_header* header, const Idmap_data_header* data_header, const Idmap_target_entry* target_entries, const Idmap_target_entry_inline* target_inline_entries, const Idmap_overlay_entry* overlay_entries, - ResStringPool* string_pool); + std::unique_ptr<ResStringPool>&& string_pool, + std::string_view overlay_apk_path, + std::string_view target_apk_path); friend OverlayStringPool; }; diff --git a/libs/androidfw/include/androidfw/LoadedArsc.h b/libs/androidfw/include/androidfw/LoadedArsc.h index 17d97a2a2e73..d9225cd812ef 100644 --- a/libs/androidfw/include/androidfw/LoadedArsc.h +++ b/libs/androidfw/include/androidfw/LoadedArsc.h @@ -47,18 +47,19 @@ class DynamicPackageEntry { // TypeSpec is going to be immediately proceeded by // an array of Type structs, all in the same block of memory. struct TypeSpec { - // Pointer to the mmapped data where flags are kept. - // Flags denote whether the resource entry is public - // and under which configurations it varies. - incfs::verified_map_ptr<ResTable_typeSpec> type_spec; + struct TypeEntry { + incfs::verified_map_ptr<ResTable_type> type; + + // Type configurations are accessed frequently when setting up an AssetManager and querying + // resources. Access this cached configuration to minimize page faults. + ResTable_config config; + }; - // The number of types that follow this struct. - // There is a type for each configuration that entries are defined for. - size_t type_count; + // Pointer to the mmapped data where flags are kept. Flags denote whether the resource entry is + // public and under which configurations it varies. + incfs::verified_map_ptr<ResTable_typeSpec> type_spec; - // Trick to easily access a variable number of Type structs - // proceeding this struct, and to ensure their alignment. - incfs::verified_map_ptr<ResTable_type> types[0]; + std::vector<TypeEntry> type_entries; base::expected<uint32_t, NullOrIOError> GetFlagsForEntryIndex(uint16_t entry_index) const { if (entry_index >= dtohl(type_spec->entryCount)) { @@ -92,11 +93,6 @@ enum : package_property_t { PROPERTY_OVERLAY = 1U << 3U, }; -// TypeSpecPtr points to a block of memory that holds a TypeSpec struct, followed by an array of -// ResTable_type pointers. -// TypeSpecPtr is a managed pointer that knows how to delete itself. -using TypeSpecPtr = util::unique_cptr<TypeSpec>; - struct OverlayableInfo { std::string name; std::string actor; @@ -239,17 +235,17 @@ class LoadedPackage { inline const TypeSpec* GetTypeSpecByTypeIndex(uint8_t type_index) const { // If the type IDs are offset in this package, we need to take that into account when searching // for a type. - return type_specs_[type_index - type_id_offset_].get(); + const auto& type_spec = type_specs_.find(type_index + 1 - type_id_offset_); + if (type_spec == type_specs_.end()) { + return nullptr; + } + return &type_spec->second; } template <typename Func> void ForEachTypeSpec(Func f) const { - for (size_t i = 0; i < type_specs_.size(); i++) { - const TypeSpecPtr& ptr = type_specs_[i]; - if (ptr != nullptr) { - uint8_t type_id = ptr->type_spec->id; - f(ptr.get(), type_id - 1); - } + for (const auto& type_spec : type_specs_) { + f(type_spec.second, type_spec.first); } } @@ -289,7 +285,7 @@ class LoadedPackage { int type_id_offset_ = 0; package_property_t property_flags_ = 0U; - ByteBucketArray<TypeSpecPtr> type_specs_; + std::unordered_map<uint8_t, TypeSpec> type_specs_; ByteBucketArray<uint32_t> resource_ids_; std::vector<DynamicPackageEntry> dynamic_package_map_; std::vector<const std::pair<OverlayableInfo, std::unordered_set<uint32_t>>> overlayable_infos_; @@ -304,17 +300,14 @@ class LoadedArsc { public: // Load a resource table from memory pointed to by `data` of size `len`. // The lifetime of `data` must out-live the LoadedArsc returned from this method. - // If `system` is set to true, the LoadedArsc is considered as a system provided resource. - // If `load_as_shared_library` is set to true, the application package (0x7f) is treated - // as a shared library (0x00). When loaded into an AssetManager, the package will be assigned an - // ID. - static std::unique_ptr<const LoadedArsc> Load(incfs::map_ptr<void> data, - size_t length, - const LoadedIdmap* loaded_idmap = nullptr, - package_property_t property_flags = 0U); + + static std::unique_ptr<LoadedArsc> Load(incfs::map_ptr<void> data, + size_t length, + const LoadedIdmap* loaded_idmap = nullptr, + package_property_t property_flags = 0U); // Create an empty LoadedArsc. This is used when an APK has no resources.arsc. - static std::unique_ptr<const LoadedArsc> CreateEmpty(); + static std::unique_ptr<LoadedArsc> CreateEmpty(); // Returns the string pool where all string resource values // (Res_value::dataType == Res_value::TYPE_STRING) are indexed. diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index fb5f86473189..bfd564c258ee 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -44,7 +44,7 @@ namespace android { constexpr const static uint32_t kIdmapMagic = 0x504D4449u; -constexpr const static uint32_t kIdmapCurrentVersion = 0x00000005u; +constexpr const static uint32_t kIdmapCurrentVersion = 0x00000007u; /** * In C++11, char16_t is defined as *at least* 16 bits. We do a lot of @@ -1700,56 +1700,6 @@ inline ResTable_overlayable_policy_header::PolicyFlags& operator |=( return first; } -struct Idmap_header { - // Always 0x504D4449 ('IDMP') - uint32_t magic; - - uint32_t version; - - uint32_t target_crc32; - uint32_t overlay_crc32; - - uint32_t fulfilled_policies; - uint32_t enforce_overlayable; - - uint8_t target_path[256]; - uint8_t overlay_path[256]; - - uint32_t debug_info_size; - uint8_t debug_info[0]; - - size_t Size() const; -}; - -struct Idmap_data_header { - uint8_t target_package_id; - uint8_t overlay_package_id; - - // Padding to ensure 4 byte alignment for target_entry_count - uint16_t p0; - - uint32_t target_entry_count; - uint32_t target_inline_entry_count; - uint32_t overlay_entry_count; - - uint32_t string_pool_index_offset; -}; - -struct Idmap_target_entry { - uint32_t target_id; - uint32_t overlay_id; -}; - -struct Idmap_target_entry_inline { - uint32_t target_id; - Res_value value; -}; - -struct Idmap_overlay_entry { - uint32_t overlay_id; - uint32_t target_id; -}; - class AssetManager2; /** diff --git a/libs/androidfw/include/androidfw/Util.h b/libs/androidfw/include/androidfw/Util.h index aceeeccccb61..c59b5b6c51a2 100644 --- a/libs/androidfw/include/androidfw/Util.h +++ b/libs/androidfw/include/androidfw/Util.h @@ -128,10 +128,14 @@ std::string Utf16ToUtf8(const StringPiece16& utf16); std::vector<std::string> SplitAndLowercase(const android::StringPiece& str, char sep); template <typename T> -bool IsFourByteAligned(const incfs::map_ptr<T>& data) { +inline bool IsFourByteAligned(const incfs::map_ptr<T>& data) { return ((size_t)data.unsafe_ptr() & 0x3U) == 0; } +inline bool IsFourByteAligned(const void* data) { + return ((size_t)data & 0x3U) == 0; +} + } // namespace util } // namespace android diff --git a/libs/androidfw/tests/AssetManager2_test.cpp b/libs/androidfw/tests/AssetManager2_test.cpp index 471b0ee1e7e9..e1c0fab77884 100644 --- a/libs/androidfw/tests/AssetManager2_test.cpp +++ b/libs/androidfw/tests/AssetManager2_test.cpp @@ -55,6 +55,12 @@ class AssetManager2Test : public ::testing::Test { basic_de_fr_assets_ = ApkAssets::Load("basic/basic_de_fr.apk"); ASSERT_NE(nullptr, basic_de_fr_assets_); + basic_xhdpi_assets_ = ApkAssets::Load("basic/basic_xhdpi-v4.apk"); + ASSERT_NE(nullptr, basic_de_fr_assets_); + + basic_xxhdpi_assets_ = ApkAssets::Load("basic/basic_xxhdpi-v4.apk"); + ASSERT_NE(nullptr, basic_de_fr_assets_); + style_assets_ = ApkAssets::Load("styles/styles.apk"); ASSERT_NE(nullptr, style_assets_); @@ -87,6 +93,8 @@ class AssetManager2Test : public ::testing::Test { protected: std::unique_ptr<const ApkAssets> basic_assets_; std::unique_ptr<const ApkAssets> basic_de_fr_assets_; + std::unique_ptr<const ApkAssets> basic_xhdpi_assets_; + std::unique_ptr<const ApkAssets> basic_xxhdpi_assets_; std::unique_ptr<const ApkAssets> style_assets_; std::unique_ptr<const ApkAssets> lib_one_assets_; std::unique_ptr<const ApkAssets> lib_two_assets_; @@ -225,6 +233,24 @@ TEST_F(AssetManager2Test, GetSharedLibraryResourceName) { ASSERT_EQ("com.android.lib_one:string/foo", ToFormattedResourceString(*name)); } +TEST_F(AssetManager2Test, GetResourceNameNonMatchingConfig) { + AssetManager2 assetmanager; + assetmanager.SetApkAssets({basic_de_fr_assets_.get()}); + + auto value = assetmanager.GetResourceName(basic::R::string::test1); + ASSERT_TRUE(value.has_value()); + EXPECT_EQ("com.android.basic:string/test1", ToFormattedResourceString(*value)); +} + +TEST_F(AssetManager2Test, GetResourceTypeSpecFlags) { + AssetManager2 assetmanager; + assetmanager.SetApkAssets({basic_de_fr_assets_.get()}); + + auto value = assetmanager.GetResourceTypeSpecFlags(basic::R::string::test1); + ASSERT_TRUE(value.has_value()); + EXPECT_EQ(ResTable_typeSpec::SPEC_PUBLIC | ResTable_config::CONFIG_LOCALE, *value); +} + TEST_F(AssetManager2Test, FindsBagResourceFromSingleApkAssets) { AssetManager2 assetmanager; assetmanager.SetApkAssets({basic_assets_.get()}); @@ -442,6 +468,29 @@ TEST_F(AssetManager2Test, ResolveDeepIdReference) { EXPECT_EQ(*low_ref, value->resid); } +TEST_F(AssetManager2Test, DensityOverride) { + AssetManager2 assetmanager; + assetmanager.SetApkAssets({basic_assets_.get(), basic_xhdpi_assets_.get(), + basic_xxhdpi_assets_.get()}); + assetmanager.SetConfiguration({ + .density = ResTable_config::DENSITY_XHIGH, + .sdkVersion = 21, + }); + + auto value = assetmanager.GetResource(basic::R::string::density, false /*may_be_bag*/); + ASSERT_TRUE(value.has_value()); + EXPECT_EQ(Res_value::TYPE_STRING, value->type); + EXPECT_EQ("xhdpi", GetStringFromPool(assetmanager.GetStringPoolForCookie(value->cookie), + value->data)); + + value = assetmanager.GetResource(basic::R::string::density, false /*may_be_bag*/, + ResTable_config::DENSITY_XXHIGH); + ASSERT_TRUE(value.has_value()); + EXPECT_EQ(Res_value::TYPE_STRING, value->type); + EXPECT_EQ("xxhdpi", GetStringFromPool(assetmanager.GetStringPoolForCookie(value->cookie), + value->data)); +} + TEST_F(AssetManager2Test, KeepLastReferenceIdUnmodifiedIfNoReferenceIsResolved) { AssetManager2 assetmanager; assetmanager.SetApkAssets({basic_assets_.get()}); @@ -716,7 +765,7 @@ TEST_F(AssetManager2Test, GetLastPathWithSingleApkAssets) { auto result = assetmanager.GetLastResourceResolution(); EXPECT_EQ("Resolution for 0x7f030000 com.android.basic:string/test1\n" - "\tFor config -de\n\tFound initial: com.android.basic", result); + "\tFor config -de\n\tFound initial: com.android.basic (basic/basic.apk)", result); } TEST_F(AssetManager2Test, GetLastPathWithMultipleApkAssets) { @@ -736,8 +785,8 @@ TEST_F(AssetManager2Test, GetLastPathWithMultipleApkAssets) { auto result = assetmanager.GetLastResourceResolution(); EXPECT_EQ("Resolution for 0x7f030000 com.android.basic:string/test1\n" "\tFor config -de\n" - "\tFound initial: com.android.basic\n" - "\tFound better: com.android.basic -de", result); + "\tFound initial: com.android.basic (basic/basic.apk)\n" + "\tFound better: com.android.basic (basic/basic_de_fr.apk) -de", result); } TEST_F(AssetManager2Test, GetLastPathAfterDisablingReturnsEmpty) { 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/androidfw/tests/Idmap_test.cpp b/libs/androidfw/tests/Idmap_test.cpp index 3f0c7cbc8ffc..b43491548e2b 100644 --- a/libs/androidfw/tests/Idmap_test.cpp +++ b/libs/androidfw/tests/Idmap_test.cpp @@ -27,6 +27,8 @@ #include "data/overlayable/R.h" #include "data/system/R.h" +using ::testing::NotNull; + namespace overlay = com::android::overlay; namespace overlayable = com::android::overlayable; @@ -195,7 +197,11 @@ TEST_F(IdmapTest, OverlaidResourceHasSameName) { } TEST_F(IdmapTest, OverlayLoaderInterop) { - auto loader_assets = ApkAssets::LoadTable("loader/resources.arsc", PROPERTY_LOADER); + auto asset = AssetsProvider::CreateAssetFromFile(GetTestDataPath() + "/loader/resources.arsc"); + ASSERT_THAT(asset, NotNull()); + + auto loader_assets = ApkAssets::LoadTable(std::move(asset), EmptyAssetsProvider::Create(), + PROPERTY_LOADER); AssetManager2 asset_manager; asset_manager.SetApkAssets({overlayable_assets_.get(), loader_assets.get(), overlay_assets_.get()}); diff --git a/libs/androidfw/tests/LoadedArsc_test.cpp b/libs/androidfw/tests/LoadedArsc_test.cpp index 63574110a817..f356c8130080 100644 --- a/libs/androidfw/tests/LoadedArsc_test.cpp +++ b/libs/androidfw/tests/LoadedArsc_test.cpp @@ -65,10 +65,10 @@ TEST(LoadedArscTest, LoadSinglePackageArsc) { const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index); ASSERT_THAT(type_spec, NotNull()); - ASSERT_THAT(type_spec->type_count, Ge(1u)); + ASSERT_THAT(type_spec->type_entries.size(), Ge(1u)); - auto type = type_spec->types[0]; - ASSERT_TRUE(LoadedPackage::GetEntry(type, entry_index).has_value()); + auto type = type_spec->type_entries[0]; + ASSERT_TRUE(LoadedPackage::GetEntry(type.type, entry_index).has_value()); } TEST(LoadedArscTest, LoadSparseEntryApp) { @@ -89,10 +89,10 @@ TEST(LoadedArscTest, LoadSparseEntryApp) { const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index); ASSERT_THAT(type_spec, NotNull()); - ASSERT_THAT(type_spec->type_count, Ge(1u)); + ASSERT_THAT(type_spec->type_entries.size(), Ge(1u)); - auto type = type_spec->types[0]; - ASSERT_TRUE(LoadedPackage::GetEntry(type, entry_index).has_value()); + auto type = type_spec->type_entries[0]; + ASSERT_TRUE(LoadedPackage::GetEntry(type.type, entry_index).has_value()); } TEST(LoadedArscTest, LoadSharedLibrary) { @@ -176,13 +176,13 @@ TEST(LoadedArscTest, LoadFeatureSplit) { const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index); ASSERT_THAT(type_spec, NotNull()); - ASSERT_THAT(type_spec->type_count, Ge(1u)); + ASSERT_THAT(type_spec->type_entries.size(), Ge(1u)); auto type_name16 = package->GetTypeStringPool()->stringAt(type_spec->type_spec->id - 1); ASSERT_TRUE(type_name16.has_value()); EXPECT_THAT(util::Utf16ToUtf8(*type_name16), StrEq("string")); - ASSERT_TRUE(LoadedPackage::GetEntry(type_spec->types[0], entry_index).has_value()); + ASSERT_TRUE(LoadedPackage::GetEntry(type_spec->type_entries[0].type, entry_index).has_value()); } // AAPT(2) generates resource tables with chunks in a certain order. The rule is that @@ -217,11 +217,11 @@ TEST(LoadedArscTest, LoadOutOfOrderTypeSpecs) { const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(0); ASSERT_THAT(type_spec, NotNull()); - ASSERT_THAT(type_spec->type_count, Ge(1u)); + ASSERT_THAT(type_spec->type_entries.size(), Ge(1u)); type_spec = package->GetTypeSpecByTypeIndex(1); ASSERT_THAT(type_spec, NotNull()); - ASSERT_THAT(type_spec->type_count, Ge(1u)); + ASSERT_THAT(type_spec->type_entries.size(), Ge(1u)); } TEST(LoadedArscTest, LoadOverlayable) { @@ -339,10 +339,8 @@ TEST(LoadedArscTest, GetOverlayableMap) { } TEST(LoadedArscTest, LoadCustomLoader) { - std::string contents; - - std::unique_ptr<Asset> - asset = ApkAssets::CreateAssetFromFile(GetTestDataPath() + "/loader/resources.arsc"); + auto asset = AssetsProvider::CreateAssetFromFile(GetTestDataPath() + "/loader/resources.arsc"); + ASSERT_THAT(asset, NotNull()); const StringPiece data( reinterpret_cast<const char*>(asset->getBuffer(true /*wordAligned*/)), @@ -363,10 +361,10 @@ TEST(LoadedArscTest, LoadCustomLoader) { const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index); ASSERT_THAT(type_spec, NotNull()); - ASSERT_THAT(type_spec->type_count, Ge(1u)); + ASSERT_THAT(type_spec->type_entries.size(), Ge(1u)); - auto type = type_spec->types[0]; - ASSERT_TRUE(LoadedPackage::GetEntry(type, entry_index).has_value()); + auto type = type_spec->type_entries[0]; + ASSERT_TRUE(LoadedPackage::GetEntry(type.type, entry_index).has_value()); } // structs with size fields (like Res_value, ResTable_entry) should be diff --git a/libs/androidfw/tests/data/app/app.apk b/libs/androidfw/tests/data/app/app.apk Binary files differindex c8ad86ded851..67036959d185 100644 --- a/libs/androidfw/tests/data/app/app.apk +++ b/libs/androidfw/tests/data/app/app.apk diff --git a/libs/androidfw/tests/data/overlay/overlay.idmap b/libs/androidfw/tests/data/overlay/overlay.idmap Binary files differindex 3ab244eb084a..723413c3cea8 100644 --- a/libs/androidfw/tests/data/overlay/overlay.idmap +++ b/libs/androidfw/tests/data/overlay/overlay.idmap diff --git a/libs/hostgraphics/Android.bp b/libs/hostgraphics/Android.bp index e713b98b867e..3a99d41448f2 100644 --- a/libs/hostgraphics/Android.bp +++ b/libs/hostgraphics/Android.bp @@ -5,6 +5,12 @@ cc_library_host_static { "-Wno-unused-parameter", ], + static_libs: [ + "libbase", + "libmath", + "libutils", + ], + srcs: [ ":libui_host_common", "Fence.cpp", @@ -20,6 +26,7 @@ cc_library_host_static { "frameworks/native/libs/nativebase/include", "frameworks/native/libs/nativewindow/include", "frameworks/native/libs/arect/include", + "frameworks/native/libs/ui/include_private", ], export_include_dirs: ["."], @@ -28,4 +35,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..ce1d96c167d7 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,47 @@ 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, + }, + // TODO: remove this + unsafe_ignore_missing_latest_api: true, +} + +filegroup { + name: "framework-graphics-srcs", + srcs: [ + "apex/java/**/*.java", + ], + path: "apex/java" +} + +// ------------------------ // APEX // ------------------------ @@ -287,13 +329,16 @@ cc_defaults { "jni/PathMeasure.cpp", "jni/Picture.cpp", "jni/Shader.cpp", + "jni/RenderEffect.cpp", "jni/Typeface.cpp", "jni/Utils.cpp", "jni/YuvToJpegEncoder.cpp", "jni/fonts/Font.cpp", "jni/fonts/FontFamily.cpp", + "jni/fonts/NativeFont.cpp", "jni/text/LineBreaker.cpp", "jni/text/MeasuredText.cpp", + "jni/text/TextShaper.cpp", ], header_libs: [ "android_graphics_jni_headers" ], @@ -346,6 +391,7 @@ cc_defaults { "libstatspull", "libstatssocket", "libpdfium", + "libbinder_ndk", ], static_libs: [ "libgif", @@ -386,6 +432,10 @@ cc_defaults { whole_static_libs: ["libskia"], srcs: [ + "canvas/CanvasFrontend.cpp", + "canvas/CanvasOpBuffer.cpp", + "canvas/CanvasOpRasterizer.cpp", + "effects/StretchEffect.cpp", "pipeline/skia/SkiaDisplayList.cpp", "pipeline/skia/SkiaRecordingCanvas.cpp", "pipeline/skia/RenderNodeDrawable.cpp", @@ -457,6 +507,7 @@ cc_defaults { "service/GraphicsStatsService.cpp", "thread/CommonPool.cpp", "utils/GLUtils.cpp", + "utils/NdkUtils.cpp", "utils/StringUtils.cpp", "AutoBackendTextureRelease.cpp", "DeferredLayerUpdater.cpp", @@ -499,6 +550,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 +572,7 @@ cc_defaults { android: { shared_libs: [ "libgui", + "libui", ], } }, @@ -554,6 +611,8 @@ cc_test { "tests/unit/ABitmapTests.cpp", "tests/unit/CacheManagerTests.cpp", "tests/unit/CanvasContextTests.cpp", + "tests/unit/CanvasOpTests.cpp", + "tests/unit/CanvasFrontendTests.cpp", "tests/unit/CommonPoolTests.cpp", "tests/unit/DamageAccumulatorTests.cpp", "tests/unit/DeferredLayerUpdaterTests.cpp", @@ -562,6 +621,7 @@ cc_test { "tests/unit/LayerUpdateQueueTests.cpp", "tests/unit/LinearAllocatorTests.cpp", "tests/unit/MatrixTests.cpp", + "tests/unit/OpBufferTests.cpp", "tests/unit/PathInterpolatorTests.cpp", "tests/unit/RenderNodeDrawableTests.cpp", "tests/unit/RenderNodeTests.cpp", @@ -616,6 +676,7 @@ cc_benchmark { srcs: [ "tests/microbench/main.cpp", + "tests/microbench/CanvasOpBench.cpp", "tests/microbench/DisplayListCanvasBench.cpp", "tests/microbench/LinearAllocatorBench.cpp", "tests/microbench/PathParserBench.cpp", 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/DisplayList.h b/libs/hwui/DisplayList.h index dc63e5db4a70..3aa5b4bf72f8 100644 --- a/libs/hwui/DisplayList.h +++ b/libs/hwui/DisplayList.h @@ -18,6 +18,8 @@ #include "pipeline/skia/SkiaDisplayList.h" +#include <memory> + namespace android { namespace uirenderer { @@ -29,7 +31,119 @@ typedef uirenderer::VectorDrawable::Tree VectorDrawableRoot; /** * Data structure that holds the list of commands used in display list stream */ -using DisplayList = skiapipeline::SkiaDisplayList; +//using DisplayList = skiapipeline::SkiaDisplayList; +class DisplayList { +public: + // Constructs an empty (invalid) DisplayList + explicit DisplayList() {} + + // Constructs a DisplayList from a SkiaDisplayList + explicit DisplayList(std::unique_ptr<skiapipeline::SkiaDisplayList> impl) + : mImpl(std::move(impl)) {} + + // Move support + DisplayList(DisplayList&& other) : mImpl(std::move(other.mImpl)) {} + DisplayList& operator=(DisplayList&& other) { + mImpl = std::move(other.mImpl); + return *this; + } + + // No copy support + DisplayList(const DisplayList& other) = delete; + DisplayList& operator=(const DisplayList&) = delete; + + void updateChildren(std::function<void(RenderNode*)> updateFn) { + mImpl->updateChildren(std::move(updateFn)); + } + + [[nodiscard]] explicit operator bool() const { + return mImpl.get() != nullptr; + } + + // If true this DisplayList contains a backing content, even if that content is empty + // If false, there this DisplayList is in an "empty" state + [[nodiscard]] bool isValid() const { + return mImpl.get() != nullptr; + } + + [[nodiscard]] bool isEmpty() const { + return !hasContent(); + } + + [[nodiscard]] bool hasContent() const { + return mImpl && !(mImpl->isEmpty()); + } + + [[nodiscard]] bool containsProjectionReceiver() const { + return mImpl && mImpl->containsProjectionReceiver(); + } + + [[nodiscard]] skiapipeline::SkiaDisplayList* asSkiaDl() { + return mImpl.get(); + } + + [[nodiscard]] const skiapipeline::SkiaDisplayList* asSkiaDl() const { + return mImpl.get(); + } + + [[nodiscard]] bool hasVectorDrawables() const { + return mImpl && mImpl->hasVectorDrawables(); + } + + void clear(RenderNode* owningNode = nullptr) { + if (mImpl && owningNode && mImpl->reuseDisplayList(owningNode)) { + // TODO: This is a bit sketchy to have a unique_ptr temporarily owned twice + // Do something to cleanup reuseDisplayList passing itself to the RenderNode + mImpl.release(); + } else { + mImpl = nullptr; + } + } + + [[nodiscard]] size_t getUsedSize() const { + return mImpl ? mImpl->getUsedSize() : 0; + } + + [[nodiscard]] size_t getAllocatedSize() const { + return mImpl ? mImpl->getAllocatedSize() : 0; + } + + void output(std::ostream& output, uint32_t level) const { + if (mImpl) { + mImpl->output(output, level); + } + } + + [[nodiscard]] bool hasFunctor() const { + return mImpl && mImpl->hasFunctor(); + } + + bool prepareListAndChildren( + TreeObserver& observer, TreeInfo& info, bool functorsNeedLayer, + std::function<void(RenderNode*, TreeObserver&, TreeInfo&, bool)> childFn) { + return mImpl && mImpl->prepareListAndChildren( + observer, info, functorsNeedLayer, std::move(childFn)); + } + + void syncContents(const WebViewSyncData& data) { + if (mImpl) { + mImpl->syncContents(data); + } + } + + [[nodiscard]] bool hasText() const { + return mImpl && mImpl->hasText(); + } + + void applyColorTransform(ColorTransform transform) { + if (mImpl) { + mImpl->mDisplayList.applyColorTransform(transform); + } + } + +private: + std::unique_ptr<skiapipeline::SkiaDisplayList> mImpl; +}; } // namespace uirenderer } // namespace android diff --git a/libs/hwui/DisplayListOps.in b/libs/hwui/DisplayListOps.in index 49817925d9b4..1b1be4311498 100644 --- a/libs/hwui/DisplayListOps.in +++ b/libs/hwui/DisplayListOps.in @@ -19,7 +19,6 @@ X(Save) X(Restore) X(SaveLayer) X(SaveBehind) -X(Concat44) X(Concat) X(SetMatrix) X(Scale) @@ -41,7 +40,6 @@ X(DrawAnnotation) X(DrawDrawable) X(DrawPicture) X(DrawImage) -X(DrawImageNine) X(DrawImageRect) X(DrawImageLattice) X(DrawTextBlob) diff --git a/libs/hwui/FrameInfo.cpp b/libs/hwui/FrameInfo.cpp index 0698775b0021..8b20492543f7 100644 --- a/libs/hwui/FrameInfo.cpp +++ b/libs/hwui/FrameInfo.cpp @@ -20,8 +20,9 @@ namespace android { namespace uirenderer { -const std::string FrameInfoNames[] = { +const std::array<std::string, static_cast<int>(FrameInfoIndex::NumIndexes)> FrameInfoNames = { "Flags", + "FrameTimelineVsyncId", "IntendedVsync", "Vsync", "OldestInputEvent", @@ -30,6 +31,7 @@ const std::string FrameInfoNames[] = { "AnimationStart", "PerformTraversalsStart", "DrawStart", + "FrameDeadline", "SyncQueued", "SyncStart", "IssueDrawCommandsStart", @@ -40,11 +42,7 @@ const std::string FrameInfoNames[] = { "GpuCompleted", }; -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..ee7d15a2fbce 100644 --- a/libs/hwui/FrameInfo.h +++ b/libs/hwui/FrameInfo.h @@ -21,16 +21,18 @@ #include <cutils/compiler.h> #include <utils/Timers.h> +#include <array> #include <memory.h> #include <string> 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 +41,7 @@ enum class FrameInfoIndex { AnimationStart, PerformTraversalsStart, DrawStart, + FrameDeadline, // End of UI frame info SyncQueued, @@ -58,7 +61,7 @@ enum class FrameInfoIndex { NumIndexes }; -extern const std::string FrameInfoNames[]; +extern const std::array<std::string, static_cast<int>(FrameInfoIndex::NumIndexes)> FrameInfoNames; namespace FrameInfoFlags { enum { @@ -69,13 +72,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 +93,7 @@ public: set(FrameInfoIndex::AnimationStart) = vsyncTime; set(FrameInfoIndex::PerformTraversalsStart) = vsyncTime; set(FrameInfoIndex::DrawStart) = vsyncTime; + set(FrameInfoIndex::FrameDeadline) = frameDeadline; return *this; } diff --git a/libs/hwui/HardwareBitmapUploader.cpp b/libs/hwui/HardwareBitmapUploader.cpp index a3d552faeb0a..859a5556323d 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; @@ -358,13 +362,8 @@ static SkBitmap makeHwCompatible(const FormatInfo& format, const SkBitmap& sourc return source; } else { SkBitmap bitmap; - const SkImageInfo& info = source.info(); - bitmap.allocPixels(info.makeColorType(kN32_SkColorType)); - - SkCanvas canvas(bitmap); - canvas.drawColor(0); - canvas.drawBitmap(source, 0.0f, 0.0f, nullptr); - + bitmap.allocPixels(source.info().makeColorType(kN32_SkColorType)); + bitmap.writePixels(source.pixmap()); return bitmap; } } @@ -394,35 +393,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/JankTracker.cpp b/libs/hwui/JankTracker.cpp index b2c39c90071a..ccce403ecfac 100644 --- a/libs/hwui/JankTracker.cpp +++ b/libs/hwui/JankTracker.cpp @@ -183,7 +183,6 @@ void JankTracker::finishFrame(const FrameInfo& frame) { ALOGI("%s", ss.str().c_str()); // Just so we have something that counts up, the value is largely irrelevant ATRACE_INT(ss.str().c_str(), ++sDaveyCount); - android::util::stats_write(android::util::DAVEY_OCCURRED, getuid(), ns2ms(totalDuration)); } } diff --git a/libs/hwui/Layer.cpp b/libs/hwui/Layer.cpp index c174c240ff22..ca2ada9e8141 100644 --- a/libs/hwui/Layer.cpp +++ b/libs/hwui/Layer.cpp @@ -18,6 +18,7 @@ #include "renderstate/RenderState.h" #include "utils/Color.h" +#include "utils/MathUtils.h" namespace android { namespace uirenderer { @@ -52,5 +53,91 @@ SkBlendMode Layer::getMode() const { } } +static inline SkScalar isIntegerAligned(SkScalar x) { + return fabsf(roundf(x) - x) <= NON_ZERO_EPSILON; +} + +// Disable filtering when there is no scaling in screen coordinates and the corners have the same +// fraction (for translate) or zero fraction (for any other rect-to-rect transform). +static bool shouldFilterRect(const SkMatrix& matrix, const SkRect& srcRect, const SkRect& dstRect) { + if (!matrix.rectStaysRect()) return true; + SkRect dstDevRect = matrix.mapRect(dstRect); + float dstW, dstH; + if (MathUtils::isZero(matrix.getScaleX()) && MathUtils::isZero(matrix.getScaleY())) { + // Has a 90 or 270 degree rotation, although total matrix may also have scale factors + // in m10 and m01. Those scalings are automatically handled by mapRect so comparing + // dimensions is sufficient, but swap width and height comparison. + dstW = dstDevRect.height(); + dstH = dstDevRect.width(); + } else { + // Handle H/V flips or 180 rotation matrices. Axes may have been mirrored, but + // dimensions are still safe to compare directly. + dstW = dstDevRect.width(); + dstH = dstDevRect.height(); + } + if (!(MathUtils::areEqual(dstW, srcRect.width()) && + MathUtils::areEqual(dstH, srcRect.height()))) { + return true; + } + // Device rect and source rect should be integer aligned to ensure there's no difference + // in how nearest-neighbor sampling is resolved. + return !(isIntegerAligned(srcRect.x()) && + isIntegerAligned(srcRect.y()) && + isIntegerAligned(dstDevRect.x()) && + isIntegerAligned(dstDevRect.y())); +} + +void Layer::draw(SkCanvas* canvas) { + GrRecordingContext* context = canvas->recordingContext(); + if (context == nullptr) { + SkDEBUGF(("Attempting to draw LayerDrawable into an unsupported surface")); + return; + } + SkMatrix layerTransform = getTransform(); + //sk_sp<SkImage> layerImage = getImage(); + const int layerWidth = getWidth(); + const int layerHeight = getHeight(); + if (layerImage) { + SkMatrix textureMatrixInv; + textureMatrixInv = getTexTransform(); + // TODO: after skia bug https://bugs.chromium.org/p/skia/issues/detail?id=7075 is fixed + // use bottom left origin and remove flipV and invert transformations. + SkMatrix flipV; + flipV.setAll(1, 0, 0, 0, -1, 1, 0, 0, 1); + textureMatrixInv.preConcat(flipV); + textureMatrixInv.preScale(1.0f / layerWidth, 1.0f / layerHeight); + textureMatrixInv.postScale(layerImage->width(), layerImage->height()); + SkMatrix textureMatrix; + if (!textureMatrixInv.invert(&textureMatrix)) { + textureMatrix = textureMatrixInv; + } + + SkMatrix matrix; + matrix = SkMatrix::Concat(layerTransform, textureMatrix); + + SkPaint paint; + paint.setAlpha(getAlpha()); + paint.setBlendMode(getMode()); + paint.setColorFilter(getColorFilter()); + const bool nonIdentityMatrix = !matrix.isIdentity(); + if (nonIdentityMatrix) { + canvas->save(); + canvas->concat(matrix); + } + const SkMatrix& totalMatrix = canvas->getTotalMatrix(); + + SkRect imageRect = SkRect::MakeIWH(layerImage->width(), layerImage->height()); + SkSamplingOptions sampling; + if (getForceFilter() || shouldFilterRect(totalMatrix, imageRect, imageRect)) { + sampling = SkSamplingOptions(SkFilterMode::kLinear); + } + canvas->drawImage(layerImage.get(), 0, 0, sampling, &paint); + // restore the original matrix + if (nonIdentityMatrix) { + canvas->restore(); + } + } +} + } // namespace uirenderer } // namespace android diff --git a/libs/hwui/Layer.h b/libs/hwui/Layer.h index ea3bfc9e80cb..e99e76299317 100644 --- a/libs/hwui/Layer.h +++ b/libs/hwui/Layer.h @@ -21,6 +21,7 @@ #include <SkBlendMode.h> #include <SkColorFilter.h> #include <SkColorSpace.h> +#include <SkCanvas.h> #include <SkPaint.h> #include <SkImage.h> #include <SkMatrix.h> @@ -87,6 +88,8 @@ public: inline sk_sp<SkImage> getImage() const { return this->layerImage; } + void draw(SkCanvas* canvas); + protected: RenderState& mRenderState; 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..e798f2a2bc69 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -15,7 +15,9 @@ */ #include "Properties.h" + #include "Debug.h" +#include "log/log_main.h" #ifdef __ANDROID__ #include "HWUIProperties.sysprop.h" #endif @@ -78,6 +80,7 @@ bool Properties::isolatedProcess = false; int Properties::contextPriority = 0; int Properties::defaultRenderAhead = -1; +float Properties::defaultSdrWhitePoint = 200.f; bool Properties::load() { bool prevDebugLayersUpdates = debugLayersUpdates; @@ -126,8 +129,9 @@ bool Properties::load() { runningInEmulator = base::GetBoolProperty(PROPERTY_QEMU_KERNEL, false); - defaultRenderAhead = std::max(-1, std::min(2, base::GetIntProperty(PROPERTY_RENDERAHEAD, - render_ahead().value_or(0)))); + defaultRenderAhead = std::max( + -1, + std::min(2, base::GetIntProperty(PROPERTY_RENDERAHEAD, render_ahead().value_or(-1)))); return (prevDebugLayersUpdates != debugLayersUpdates) || (prevDebugOverdraw != debugOverdraw); } @@ -189,15 +193,12 @@ RenderPipelineType Properties::getRenderPipelineType() { return sRenderPipelineType; } -void Properties::overrideRenderPipelineType(RenderPipelineType type) { +void Properties::overrideRenderPipelineType(RenderPipelineType type, bool inUnitTest) { // If we're doing actual rendering then we can't change the renderer after it's been set. - // Unit tests can freely change this as often as it wants, though, as there's no actual - // GL rendering happening - if (sRenderPipelineType != RenderPipelineType::NotInitialized) { - LOG_ALWAYS_FATAL_IF(sRenderPipelineType != type, - "Trying to change pipeline but it's already set"); - return; - } + // Unit tests can freely change this as often as it wants. + LOG_ALWAYS_FATAL_IF(sRenderPipelineType != RenderPipelineType::NotInitialized && + sRenderPipelineType != type && !inUnitTest, + "Trying to change pipeline but it's already set."); sRenderPipelineType = type; } diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index d3ecb54d94f6..1639143ef87c 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 void overrideRenderPipelineType(RenderPipelineType, bool inUnitTest = false); 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..64b8b711f0a8 100644 --- a/libs/hwui/RecordingCanvas.cpp +++ b/libs/hwui/RecordingCanvas.cpp @@ -16,12 +16,14 @@ #include "RecordingCanvas.h" -#include "pipeline/skia/FunctorDrawable.h" -#include "VectorDrawable.h" +#include <GrRecordingContext.h> + +#include <experimental/type_traits> #include "SkAndroidFrameworkUtils.h" #include "SkCanvas.h" #include "SkCanvasPriv.h" +#include "SkColor.h" #include "SkData.h" #include "SkDrawShadowInfo.h" #include "SkImage.h" @@ -33,8 +35,8 @@ #include "SkRegion.h" #include "SkTextBlob.h" #include "SkVertices.h" - -#include <experimental/type_traits> +#include "VectorDrawable.h" +#include "pipeline/skia/FunctorDrawable.h" namespace android { namespace uirenderer { @@ -96,7 +98,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 +106,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 { @@ -130,24 +127,18 @@ 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); } -}; struct Concat final : Op { static const auto kType = Type::Concat; - Concat(const SkMatrix& matrix) : matrix(matrix) {} - SkMatrix matrix; + Concat(const SkM44& matrix) : matrix(matrix) {} + SkM44 matrix; void draw(SkCanvas* c, const SkMatrix&) const { c->concat(matrix); } }; struct SetMatrix final : Op { static const auto kType = Type::SetMatrix; - SetMatrix(const SkMatrix& matrix) : matrix(matrix) {} - SkMatrix matrix; + SetMatrix(const SkM44& matrix) : matrix(matrix) {} + SkM44 matrix; void draw(SkCanvas* c, const SkMatrix& original) const { - c->setMatrix(SkMatrix::Concat(original, matrix)); + c->setMatrix(SkM44(original) * matrix); } }; struct Scale final : Op { @@ -317,42 +308,29 @@ struct DrawPicture final : Op { struct DrawImage final : Op { static const auto kType = Type::DrawImage; - DrawImage(sk_sp<const SkImage>&& image, SkScalar x, SkScalar y, const SkPaint* paint, - BitmapPalette palette) - : image(std::move(image)), x(x), y(y), palette(palette) { + DrawImage(sk_sp<const SkImage>&& image, SkScalar x, SkScalar y, + const SkSamplingOptions& sampling, const SkPaint* paint, BitmapPalette palette) + : image(std::move(image)), x(x), y(y), sampling(sampling), palette(palette) { if (paint) { this->paint = *paint; } } sk_sp<const SkImage> image; SkScalar x, y; + SkSamplingOptions sampling; SkPaint paint; BitmapPalette palette; - void draw(SkCanvas* c, const SkMatrix&) const { c->drawImage(image.get(), x, y, &paint); } -}; -struct DrawImageNine final : Op { - static const auto kType = Type::DrawImageNine; - DrawImageNine(sk_sp<const SkImage>&& image, const SkIRect& center, const SkRect& dst, - const SkPaint* paint) - : image(std::move(image)), center(center), dst(dst) { - if (paint) { - this->paint = *paint; - } - } - sk_sp<const SkImage> image; - SkIRect center; - SkRect dst; - SkPaint paint; void draw(SkCanvas* c, const SkMatrix&) const { - c->drawImageNine(image.get(), center, dst, &paint); + c->drawImage(image.get(), x, y, sampling, &paint); } }; struct DrawImageRect final : Op { static const auto kType = Type::DrawImageRect; DrawImageRect(sk_sp<const SkImage>&& image, const SkRect* src, const SkRect& dst, - const SkPaint* paint, SkCanvas::SrcRectConstraint constraint, - BitmapPalette palette) - : image(std::move(image)), dst(dst), constraint(constraint), palette(palette) { + const SkSamplingOptions& sampling, const SkPaint* paint, + SkCanvas::SrcRectConstraint constraint, BitmapPalette palette) + : image(std::move(image)), dst(dst), sampling(sampling), constraint(constraint) + , palette(palette) { this->src = src ? *src : SkRect::MakeIWH(this->image->width(), this->image->height()); if (paint) { this->paint = *paint; @@ -360,23 +338,26 @@ struct DrawImageRect final : Op { } sk_sp<const SkImage> image; SkRect src, dst; + SkSamplingOptions sampling; SkPaint paint; SkCanvas::SrcRectConstraint constraint; BitmapPalette palette; void draw(SkCanvas* c, const SkMatrix&) const { - c->drawImageRect(image.get(), src, dst, &paint, constraint); + c->drawImageRect(image.get(), src, dst, sampling, &paint, constraint); } }; struct DrawImageLattice final : Op { static const auto kType = Type::DrawImageLattice; DrawImageLattice(sk_sp<const SkImage>&& image, int xs, int ys, int fs, const SkIRect& src, - const SkRect& dst, const SkPaint* paint, BitmapPalette palette) + const SkRect& dst, SkFilterMode filter, const SkPaint* paint, + BitmapPalette palette) : image(std::move(image)) , xs(xs) , ys(ys) , fs(fs) , src(src) , dst(dst) + , filter(filter) , palette(palette) { if (paint) { this->paint = *paint; @@ -386,6 +367,7 @@ struct DrawImageLattice final : Op { int xs, ys, fs; SkIRect src; SkRect dst; + SkFilterMode filter; SkPaint paint; BitmapPalette palette; void draw(SkCanvas* c, const SkMatrix&) const { @@ -394,7 +376,8 @@ struct DrawImageLattice final : Op { auto flags = (0 == fs) ? nullptr : pod<SkCanvas::Lattice::RectType>( this, (xs + ys) * sizeof(int) + fs * sizeof(SkColor)); - c->drawImageLattice(image.get(), {xdivs, ydivs, flags, xs, ys, &src, colors}, dst, &paint); + c->drawImageLattice(image.get(), {xdivs, ydivs, flags, xs, ys, &src, colors}, dst, + filter, &paint); } }; @@ -448,21 +431,21 @@ 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 { static const auto kType = Type::DrawAtlas; - DrawAtlas(const SkImage* atlas, int count, SkBlendMode xfermode, const SkRect* cull, - const SkPaint* paint, bool has_colors) - : atlas(sk_ref_sp(atlas)), count(count), xfermode(xfermode), has_colors(has_colors) { + DrawAtlas(const SkImage* atlas, int count, SkBlendMode mode, const SkSamplingOptions& sampling, + const SkRect* cull, const SkPaint* paint, bool has_colors) + : atlas(sk_ref_sp(atlas)), count(count), mode(mode), sampling(sampling) + , has_colors(has_colors) { if (cull) { this->cull = *cull; } @@ -472,7 +455,8 @@ struct DrawAtlas final : Op { } sk_sp<const SkImage> atlas; int count; - SkBlendMode xfermode; + SkBlendMode mode; + SkSamplingOptions sampling; SkRect cull = kUnset; SkPaint paint; bool has_colors; @@ -481,7 +465,8 @@ struct DrawAtlas final : Op { auto texs = pod<SkRect>(this, count * sizeof(SkRSXform)); auto colors = has_colors ? pod<SkColor>(this, count * (sizeof(SkRSXform) + sizeof(SkRect))) : nullptr; - c->drawAtlas(atlas.get(), xforms, texs, colors, count, xfermode, maybe_unset(cull), &paint); + c->drawAtlas(atlas.get(), xforms, texs, colors, count, mode, sampling, maybe_unset(cull), + &paint); } }; struct DrawShadowRec final : Op { @@ -502,7 +487,9 @@ struct DrawVectorDrawable final : Op { tree->getPaintFor(&paint, tree->stagingProperties()); } - void draw(SkCanvas* canvas, const SkMatrix&) const { mRoot->draw(canvas, mBounds, paint); } + void draw(SkCanvas* canvas, const SkMatrix&) const { + mRoot->draw(canvas, mBounds, paint); + } sp<VectorDrawableRoot> mRoot; SkRect mBounds; @@ -517,7 +504,68 @@ struct DrawWebView final : Op { // SkDrawable::onSnapGpuDrawHandler callback instead of SkDrawable::onDraw. // SkCanvas::drawDrawable/SkGpuDevice::drawDrawable has the logic to invoke // onSnapGpuDrawHandler. - void draw(SkCanvas* c, const SkMatrix&) const { c->drawDrawable(drawable.get()); } +private: + // Unfortunately WebView does not have complex clip information serialized, and we only perform + // best-effort stencil fill for GLES. So for Vulkan we create an intermediate layer if the + // canvas clip is complex. + static bool needsCompositedLayer(SkCanvas* c) { + if (Properties::getRenderPipelineType() != RenderPipelineType::SkiaVulkan) { + return false; + } + SkRegion clipRegion; + // WebView's rasterizer has access to simple clips, so for Vulkan we only need to check if + // the clip is more complex than a rectangle. + c->temporary_internal_getRgnClip(&clipRegion); + return clipRegion.isComplex(); + } + + mutable SkImageInfo mLayerImageInfo; + mutable sk_sp<SkSurface> mLayerSurface = nullptr; + +public: + void draw(SkCanvas* c, const SkMatrix&) const { + if (needsCompositedLayer(c)) { + // What we do now is create an offscreen surface, sized by the clip bounds. + // We won't apply a clip while drawing - clipping will be performed when compositing the + // surface back onto the original canvas. Note also that we're not using saveLayer + // because the webview functor still doesn't respect the canvas clip stack. + const SkIRect deviceBounds = c->getDeviceClipBounds(); + if (mLayerSurface == nullptr || c->imageInfo() != mLayerImageInfo) { + GrRecordingContext* directContext = c->recordingContext(); + mLayerImageInfo = + c->imageInfo().makeWH(deviceBounds.width(), deviceBounds.height()); + mLayerSurface = SkSurface::MakeRenderTarget(directContext, SkBudgeted::kYes, + mLayerImageInfo, 0, + kTopLeft_GrSurfaceOrigin, nullptr); + } + + SkCanvas* layerCanvas = mLayerSurface->getCanvas(); + + SkAutoCanvasRestore(layerCanvas, true); + layerCanvas->clear(SK_ColorTRANSPARENT); + + // Preserve the transform from the original canvas, but now the clip rectangle is + // anchored at the origin so we need to transform the clipped content to the origin. + SkM44 mat4(c->getLocalToDevice()); + mat4.postTranslate(-deviceBounds.fLeft, -deviceBounds.fTop); + layerCanvas->concat(mat4); + layerCanvas->drawDrawable(drawable.get()); + + SkAutoCanvasRestore acr(c, true); + + // Temporarily use an identity transform, because this is just blitting to the parent + // canvas with an offset. + SkMatrix invertedMatrix; + if (!c->getTotalMatrix().invert(&invertedMatrix)) { + ALOGW("Unable to extract invert canvas matrix; aborting VkFunctor draw"); + return; + } + c->concat(invertedMatrix); + mLayerSurface->draw(c, deviceBounds.fLeft, deviceBounds.fTop); + } else { + c->drawDrawable(drawable.get()); + } + } }; } @@ -530,6 +578,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,22 +614,18 @@ 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 SkMatrix& matrix) { - this->push<Concat>(0, matrix); +void DisplayListData::concat(const SkM44& m) { + this->push<Concat>(0, m); } -void DisplayListData::setMatrix(const SkMatrix& matrix) { +void DisplayListData::setMatrix(const SkM44& matrix) { this->push<SetMatrix>(0, matrix); } void DisplayListData::scale(SkScalar sx, SkScalar sy) { @@ -645,20 +690,18 @@ void DisplayListData::drawPicture(const SkPicture* picture, const SkMatrix* matr this->push<DrawPicture>(0, picture, matrix, paint); } void DisplayListData::drawImage(sk_sp<const SkImage> image, SkScalar x, SkScalar y, - const SkPaint* paint, BitmapPalette palette) { - this->push<DrawImage>(0, std::move(image), x, y, paint, palette); -} -void DisplayListData::drawImageNine(sk_sp<const SkImage> image, const SkIRect& center, - const SkRect& dst, const SkPaint* paint) { - this->push<DrawImageNine>(0, std::move(image), center, dst, paint); + const SkSamplingOptions& sampling, const SkPaint* paint, + BitmapPalette palette) { + this->push<DrawImage>(0, std::move(image), x, y, sampling, paint, palette); } void DisplayListData::drawImageRect(sk_sp<const SkImage> image, const SkRect* src, - const SkRect& dst, const SkPaint* paint, - SkCanvas::SrcRectConstraint constraint, BitmapPalette palette) { - this->push<DrawImageRect>(0, std::move(image), src, dst, paint, constraint, palette); + const SkRect& dst, const SkSamplingOptions& sampling, + const SkPaint* paint, SkCanvas::SrcRectConstraint constraint, + BitmapPalette palette) { + this->push<DrawImageRect>(0, std::move(image), src, dst, sampling, paint, constraint, palette); } void DisplayListData::drawImageLattice(sk_sp<const SkImage> image, const SkCanvas::Lattice& lattice, - const SkRect& dst, const SkPaint* paint, + const SkRect& dst, SkFilterMode filter, const SkPaint* paint, BitmapPalette palette) { int xs = lattice.fXCount, ys = lattice.fYCount; int fs = lattice.fRectTypes ? (xs + 1) * (ys + 1) : 0; @@ -666,7 +709,7 @@ void DisplayListData::drawImageLattice(sk_sp<const SkImage> image, const SkCanva fs * sizeof(SkColor); SkASSERT(lattice.fBounds); void* pod = this->push<DrawImageLattice>(bytes, std::move(image), xs, ys, fs, *lattice.fBounds, - dst, paint, palette); + dst, filter, paint, palette); copy_v(pod, lattice.fXDivs, xs, lattice.fYDivs, ys, lattice.fColors, fs, lattice.fRectTypes, fs); } @@ -686,21 +729,19 @@ 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* vert, SkBlendMode mode, const SkPaint& paint) { + this->push<DrawVertices>(0, vert, mode, paint); } void DisplayListData::drawAtlas(const SkImage* atlas, const SkRSXform xforms[], const SkRect texs[], const SkColor colors[], int count, SkBlendMode xfermode, - const SkRect* cull, const SkPaint* paint) { + const SkSamplingOptions& sampling, const SkRect* cull, + const SkPaint* paint) { size_t bytes = count * (sizeof(SkRSXform) + sizeof(SkRect)); if (colors) { bytes += count * sizeof(SkColor); } - void* pod = - this->push<DrawAtlas>(bytes, atlas, count, xfermode, cull, paint, colors != nullptr); + void* pod = this->push<DrawAtlas>(bytes, atlas, count, xfermode, sampling, cull, paint, + colors != nullptr); copy_v(pod, xforms, count, texs, count, colors, colors ? count : 0); } void DisplayListData::drawShadowRec(const SkPath& path, const SkDrawShadowRec& rec) { @@ -823,8 +864,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,13 +881,10 @@ 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); -} -void RecordingCanvas::didSetMatrix(const SkMatrix& matrix) { +void RecordingCanvas::didSetM44(const SkM44& matrix) { fDL->setMatrix(matrix); } void RecordingCanvas::didScale(SkScalar sx, SkScalar sy) { @@ -929,37 +966,21 @@ 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); + const SkSamplingOptions& sampling, const SkPaint* paint, + BitmapPalette palette) { + fDL->drawImage(image, x, y, sampling, paint, palette); } void RecordingCanvas::drawImageRect(const sk_sp<SkImage>& image, const SkRect& src, - const SkRect& dst, const SkPaint* paint, - SrcRectConstraint constraint, BitmapPalette palette) { - fDL->drawImageRect(image, &src, dst, paint, constraint, palette); + const SkRect& dst, const SkSamplingOptions& sampling, + const SkPaint* paint, SrcRectConstraint constraint, + BitmapPalette palette) { + fDL->drawImageRect(image, &src, dst, sampling, paint, constraint, palette); } void RecordingCanvas::drawImageLattice(const sk_sp<SkImage>& image, const Lattice& lattice, - const SkRect& dst, const SkPaint* paint, + const SkRect& dst, SkFilterMode filter, const SkPaint* paint, BitmapPalette palette) { if (!image || dst.isEmpty()) { return; @@ -973,28 +994,29 @@ void RecordingCanvas::drawImageLattice(const sk_sp<SkImage>& image, const Lattic } if (SkLatticeIter::Valid(image->width(), image->height(), latticePlusBounds)) { - fDL->drawImageLattice(image, latticePlusBounds, dst, paint, palette); + fDL->drawImageLattice(image, latticePlusBounds, dst, filter, paint, palette); } else { - fDL->drawImageRect(image, nullptr, dst, paint, SrcRectConstraint::kFast_SrcRectConstraint, - palette); + SkSamplingOptions sampling(filter, SkMipmapMode::kNone); + fDL->drawImageRect(image, nullptr, dst, sampling, paint, kFast_SrcRectConstraint, palette); } } -void RecordingCanvas::onDrawImage(const SkImage* img, SkScalar x, SkScalar y, - const SkPaint* paint) { - fDL->drawImage(sk_ref_sp(img), x, y, paint, BitmapPalette::Unknown); -} -void RecordingCanvas::onDrawImageNine(const SkImage* img, const SkIRect& center, const SkRect& dst, - const SkPaint* paint) { - fDL->drawImageNine(sk_ref_sp(img), center, dst, paint); +void RecordingCanvas::onDrawImage2(const SkImage* img, SkScalar x, SkScalar y, + const SkSamplingOptions& sampling, const SkPaint* paint) { + fDL->drawImage(sk_ref_sp(img), x, y, sampling, paint, BitmapPalette::Unknown); } -void RecordingCanvas::onDrawImageRect(const SkImage* img, const SkRect* src, const SkRect& dst, - const SkPaint* paint, SrcRectConstraint constraint) { - fDL->drawImageRect(sk_ref_sp(img), src, dst, paint, constraint, BitmapPalette::Unknown); + +void RecordingCanvas::onDrawImageRect2(const SkImage* img, const SkRect& src, const SkRect& dst, + const SkSamplingOptions& sampling, const SkPaint* paint, + SrcRectConstraint constraint) { + fDL->drawImageRect(sk_ref_sp(img), &src, dst, sampling, paint, constraint, + BitmapPalette::Unknown); } -void RecordingCanvas::onDrawImageLattice(const SkImage* img, const SkCanvas::Lattice& lattice, - const SkRect& dst, const SkPaint* paint) { - fDL->drawImageLattice(sk_ref_sp(img), lattice, dst, paint, BitmapPalette::Unknown); + +void RecordingCanvas::onDrawImageLattice2(const SkImage* img, const SkCanvas::Lattice& lattice, + const SkRect& dst, SkFilterMode filter, + const SkPaint* paint) { + fDL->drawImageLattice(sk_ref_sp(img), lattice, dst, filter, paint, BitmapPalette::Unknown); } void RecordingCanvas::onDrawPatch(const SkPoint cubics[12], const SkColor colors[4], @@ -1007,14 +1029,14 @@ 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, - SkBlendMode bmode, const SkRect* cull, const SkPaint* paint) { - fDL->drawAtlas(atlas, xforms, texs, colors, count, bmode, cull, paint); +void RecordingCanvas::onDrawAtlas2(const SkImage* atlas, const SkRSXform xforms[], + const SkRect texs[], const SkColor colors[], int count, + SkBlendMode bmode, const SkSamplingOptions& sampling, + const SkRect* cull, const SkPaint* paint) { + fDL->drawAtlas(atlas, xforms, texs, colors, count, bmode, sampling, cull, paint); } void RecordingCanvas::onDrawShadowRec(const SkPath& path, const SkDrawShadowRec& rec) { fDL->drawShadowRec(path, rec); diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h index 7eb1ce3eb18a..a6a7b12ba658 100644 --- a/libs/hwui/RecordingCanvas.h +++ b/libs/hwui/RecordingCanvas.h @@ -18,7 +18,6 @@ #include "CanvasTransform.h" #include "hwui/Bitmap.h" -#include "hwui/Canvas.h" #include "utils/Macros.h" #include "utils/TypeLogic.h" @@ -29,7 +28,6 @@ #include "SkPaint.h" #include "SkPath.h" #include "SkRect.h" -#include "SkTemplates.h" #include <vector> @@ -40,6 +38,11 @@ namespace skiapipeline { class FunctorDrawable; } +namespace VectorDrawable { +class Tree; +} +typedef uirenderer::VectorDrawable::Tree VectorDrawableRoot; + enum class DisplayListOpType : uint8_t { #define X(T) T, #include "DisplayListOps.in" @@ -77,14 +80,12 @@ 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 SkMatrix&); - void setMatrix(const SkMatrix&); + void concat(const SkM44&); + void setMatrix(const SkM44&); void scale(SkScalar, SkScalar); void translate(SkScalar, SkScalar); void translateZ(SkScalar); @@ -110,20 +111,20 @@ private: void drawTextBlob(const SkTextBlob*, SkScalar, SkScalar, const SkPaint&); - void drawImage(sk_sp<const SkImage>, SkScalar, SkScalar, const SkPaint*, BitmapPalette palette); + void drawImage(sk_sp<const SkImage>, SkScalar, SkScalar, const SkSamplingOptions&, + const SkPaint*, BitmapPalette palette); void drawImageNine(sk_sp<const SkImage>, const SkIRect&, const SkRect&, const SkPaint*); - void drawImageRect(sk_sp<const SkImage>, const SkRect*, const SkRect&, const SkPaint*, - SkCanvas::SrcRectConstraint, BitmapPalette palette); + void drawImageRect(sk_sp<const SkImage>, const SkRect*, const SkRect&, const SkSamplingOptions&, + const SkPaint*, SkCanvas::SrcRectConstraint, BitmapPalette palette); void drawImageLattice(sk_sp<const SkImage>, const SkCanvas::Lattice&, const SkRect&, - const SkPaint*, BitmapPalette); + SkFilterMode, const SkPaint*, BitmapPalette); 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*); + SkBlendMode, const SkSamplingOptions&, const SkRect*, const SkPaint*); void drawShadowRec(const SkPath&, const SkDrawShadowRec&); void drawVectorDrawable(VectorDrawableRoot* tree); void drawWebView(skiapipeline::FunctorDrawable*); @@ -155,9 +156,8 @@ public: void onFlush() override; - void didConcat44(const SkScalar[16]) override; - void didConcat(const SkMatrix&) override; - void didSetMatrix(const SkMatrix&) override; + void didConcat44(const SkM44&) override; + void didSetM44(const SkM44&) override; void didScale(SkScalar, SkScalar) override; void didTranslate(SkScalar, SkScalar) override; @@ -182,34 +182,27 @@ 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); + void drawImage(const sk_sp<SkImage>&, SkScalar left, SkScalar top, const SkSamplingOptions&, + const SkPaint* paint, BitmapPalette pallete); void drawImageRect(const sk_sp<SkImage>& image, const SkRect& src, const SkRect& dst, - const SkPaint* paint, SrcRectConstraint constraint, BitmapPalette palette); + const SkSamplingOptions&, const SkPaint*, SrcRectConstraint, BitmapPalette); void drawImageLattice(const sk_sp<SkImage>& image, const Lattice& lattice, const SkRect& dst, - const SkPaint* paint, BitmapPalette palette); + SkFilterMode, const SkPaint* paint, BitmapPalette palette); - void onDrawImage(const SkImage*, SkScalar, SkScalar, const SkPaint*) override; - void onDrawImageLattice(const SkImage*, const Lattice&, const SkRect&, const SkPaint*) override; - void onDrawImageNine(const SkImage*, const SkIRect&, const SkRect&, const SkPaint*) override; - void onDrawImageRect(const SkImage*, const SkRect*, const SkRect&, const SkPaint*, - SrcRectConstraint) override; + void onDrawImage2(const SkImage*, SkScalar, SkScalar, const SkSamplingOptions&, + const SkPaint*) override; + void onDrawImageLattice2(const SkImage*, const Lattice&, const SkRect&, SkFilterMode, + const SkPaint*) override; + void onDrawImageRect2(const SkImage*, const SkRect&, const SkRect&, const SkSamplingOptions&, + const SkPaint*, SrcRectConstraint) override; 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 onDrawAtlas(const SkImage*, const SkRSXform[], const SkRect[], const SkColor[], int, - SkBlendMode, const SkRect*, const SkPaint*) override; + void onDrawVerticesObject(const SkVertices*, SkBlendMode, const SkPaint&) override; + void onDrawAtlas2(const SkImage*, const SkRSXform[], const SkRect[], const SkColor[], int, + SkBlendMode, const SkSamplingOptions&, const SkRect*, const SkPaint*) override; void onDrawShadowRec(const SkPath&, const SkDrawShadowRec&) override; void drawVectorDrawable(VectorDrawableRoot* tree); diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp index 31e45558139d..44f54eef458f 100644 --- a/libs/hwui/RenderNode.cpp +++ b/libs/hwui/RenderNode.cpp @@ -70,15 +70,17 @@ RenderNode::RenderNode() RenderNode::~RenderNode() { ImmediateRemoved observer(nullptr); deleteDisplayList(observer); - delete mStagingDisplayList; LOG_ALWAYS_FATAL_IF(hasLayer(), "layer missed detachment!"); } -void RenderNode::setStagingDisplayList(DisplayList* displayList) { - mValid = (displayList != nullptr); +void RenderNode::setStagingDisplayList(DisplayList&& newData) { + mValid = newData.isValid(); mNeedsDisplayListSync = true; - delete mStagingDisplayList; - mStagingDisplayList = displayList; + mStagingDisplayList = std::move(newData); +} + +void RenderNode::discardStagingDisplayList() { + setStagingDisplayList(DisplayList()); } /** @@ -101,32 +103,22 @@ void RenderNode::output(std::ostream& output, uint32_t level) { properties().debugOutputProperties(output, level + 1); - if (mDisplayList) { - mDisplayList->output(output, level); - } + mDisplayList.output(output, level); output << std::string(level * 2, ' ') << "/RenderNode(" << getName() << " " << this << ")"; output << std::endl; } int RenderNode::getUsageSize() { int size = sizeof(RenderNode); - if (mStagingDisplayList) { - size += mStagingDisplayList->getUsedSize(); - } - if (mDisplayList && mDisplayList != mStagingDisplayList) { - size += mDisplayList->getUsedSize(); - } + size += mStagingDisplayList.getUsedSize(); + size += mDisplayList.getUsedSize(); return size; } int RenderNode::getAllocatedSize() { int size = sizeof(RenderNode); - if (mStagingDisplayList) { - size += mStagingDisplayList->getAllocatedSize(); - } - if (mDisplayList && mDisplayList != mStagingDisplayList) { - size += mDisplayList->getAllocatedSize(); - } + size += mStagingDisplayList.getAllocatedSize(); + size += mDisplayList.getAllocatedSize(); return size; } @@ -242,9 +234,9 @@ void RenderNode::prepareTreeImpl(TreeObserver& observer, TreeInfo& info, bool fu bool willHaveFunctor = false; if (info.mode == TreeInfo::MODE_FULL && mStagingDisplayList) { - willHaveFunctor = mStagingDisplayList->hasFunctor(); + willHaveFunctor = mStagingDisplayList.hasFunctor(); } else if (mDisplayList) { - willHaveFunctor = mDisplayList->hasFunctor(); + willHaveFunctor = mDisplayList.hasFunctor(); } bool childFunctorsNeedLayer = mProperties.prepareForFunctorPresence(willHaveFunctor, functorsNeedLayer); @@ -259,8 +251,8 @@ void RenderNode::prepareTreeImpl(TreeObserver& observer, TreeInfo& info, bool fu } if (mDisplayList) { - info.out.hasFunctors |= mDisplayList->hasFunctor(); - bool isDirty = mDisplayList->prepareListAndChildren( + info.out.hasFunctors |= mDisplayList.hasFunctor(); + bool isDirty = mDisplayList.prepareListAndChildren( observer, info, childFunctorsNeedLayer, [](RenderNode* child, TreeObserver& observer, TreeInfo& info, bool functorsNeedLayer) { @@ -314,16 +306,15 @@ void RenderNode::syncDisplayList(TreeObserver& observer, TreeInfo* info) { // Make sure we inc first so that we don't fluctuate between 0 and 1, // which would thrash the layer cache if (mStagingDisplayList) { - mStagingDisplayList->updateChildren([](RenderNode* child) { child->incParentRefCount(); }); + mStagingDisplayList.updateChildren([](RenderNode* child) { child->incParentRefCount(); }); } deleteDisplayList(observer, info); - mDisplayList = mStagingDisplayList; - mStagingDisplayList = nullptr; + mDisplayList = std::move(mStagingDisplayList); if (mDisplayList) { WebViewSyncData syncData { .applyForceDark = info && !info->disableForceDark }; - mDisplayList->syncContents(syncData); + mDisplayList.syncContents(syncData); handleForceDark(info); } } @@ -333,15 +324,18 @@ void RenderNode::handleForceDark(android::uirenderer::TreeInfo *info) { return; } auto usage = usageHint(); - const auto& children = mDisplayList->mChildNodes; - if (mDisplayList->hasText()) { + FatVector<RenderNode*, 6> children; + mDisplayList.updateChildren([&children](RenderNode* node) { + children.push_back(node); + }); + if (mDisplayList.hasText()) { usage = UsageHint::Foreground; } if (usage == UsageHint::Unknown) { if (children.size() > 1) { usage = UsageHint::Background; } else if (children.size() == 1 && - children.front().getRenderNode()->usageHint() != + children.front()->usageHint() != UsageHint::Background) { usage = UsageHint::Background; } @@ -350,7 +344,7 @@ void RenderNode::handleForceDark(android::uirenderer::TreeInfo *info) { // Crude overlap check SkRect drawn = SkRect::MakeEmpty(); for (auto iter = children.rbegin(); iter != children.rend(); ++iter) { - const auto& child = iter->getRenderNode(); + const auto& child = *iter; // We use stagingProperties here because we haven't yet sync'd the children SkRect bounds = SkRect::MakeXYWH(child->stagingProperties().getX(), child->stagingProperties().getY(), child->stagingProperties().getWidth(), child->stagingProperties().getHeight()); @@ -361,7 +355,7 @@ void RenderNode::handleForceDark(android::uirenderer::TreeInfo *info) { drawn.join(bounds); } } - mDisplayList->mDisplayList.applyColorTransform( + mDisplayList.applyColorTransform( usage == UsageHint::Background ? ColorTransform::Dark : ColorTransform::Light); } @@ -378,20 +372,17 @@ void RenderNode::pushStagingDisplayListChanges(TreeObserver& observer, TreeInfo& void RenderNode::deleteDisplayList(TreeObserver& observer, TreeInfo* info) { if (mDisplayList) { - mDisplayList->updateChildren( + mDisplayList.updateChildren( [&observer, info](RenderNode* child) { child->decParentRefCount(observer, info); }); - if (!mDisplayList->reuseDisplayList(this, info ? &info->canvasContext : nullptr)) { - delete mDisplayList; - } + mDisplayList.clear(this); } - mDisplayList = nullptr; } void RenderNode::destroyHardwareResources(TreeInfo* info) { if (hasLayer()) { this->setLayerSurface(nullptr); } - setStagingDisplayList(nullptr); + discardStagingDisplayList(); ImmediateRemoved observer(info); deleteDisplayList(observer, info); @@ -402,7 +393,7 @@ void RenderNode::destroyLayers() { this->setLayerSurface(nullptr); } if (mDisplayList) { - mDisplayList->updateChildren([](RenderNode* child) { child->destroyLayers(); }); + mDisplayList.updateChildren([](RenderNode* child) { child->destroyLayers(); }); } } diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h index c0ec2174bb35..39ea53b6e3b3 100644 --- a/libs/hwui/RenderNode.h +++ b/libs/hwui/RenderNode.h @@ -94,22 +94,23 @@ 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); + void discardStagingDisplayList(); - 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(); } + bool isRenderable() const { return mDisplayList.hasContent(); } bool hasProjectionReceiver() const { - return mDisplayList && mDisplayList->containsProjectionReceiver(); + return mDisplayList.containsProjectionReceiver(); } const char* getName() const { return mName.string(); } @@ -149,12 +150,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() @@ -168,18 +169,20 @@ public: bool nothingToDraw() const { const Outline& outline = properties().getOutline(); - return mDisplayList == nullptr || properties().getAlpha() <= 0 || + return !mDisplayList.isValid() || properties().getAlpha() <= 0 || (outline.getShouldClip() && outline.isEmpty()) || properties().getScaleX() == 0 || properties().getScaleY() == 0; } - const DisplayList* getDisplayList() const { return mDisplayList; } + const DisplayList& getDisplayList() const { return mDisplayList; } + // TODO: can this be cleaned up? + DisplayList& getDisplayList() { return mDisplayList; } // Note: The position callbacks are relying on the listener using // 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 +193,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); @@ -252,8 +255,8 @@ private: bool mNeedsDisplayListSync; // WARNING: Do not delete this directly, you must go through deleteDisplayList()! - DisplayList* mDisplayList; - DisplayList* mStagingDisplayList; + DisplayList mDisplayList; + DisplayList mStagingDisplayList; int64_t mDamageGenerationId; 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..609706e2e49d 100644 --- a/libs/hwui/RenderProperties.h +++ b/libs/hwui/RenderProperties.h @@ -23,10 +23,12 @@ #include "Outline.h" #include "Rect.h" #include "RevealClip.h" +#include "effects/StretchEffect.h" #include "utils/MathUtils.h" #include "utils/PaintUtils.h" #include <SkBlendMode.h> +#include <SkImageFilter.h> #include <SkCamera.h> #include <SkColor.h> #include <SkMatrix.h> @@ -69,7 +71,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 +95,14 @@ public: SkColorFilter* getColorFilter() const { return mColorFilter.get(); } + bool setImageFilter(SkImageFilter* imageFilter); + + SkImageFilter* getImageFilter() const { return mImageFilter.get(); } + + const StretchEffect& getStretchEffect() const { return mStretchEffect; } + + StretchEffect& mutableStretchEffect() { return mStretchEffect; } + // 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 +128,14 @@ private: uint8_t mAlpha; SkBlendMode mMode; sk_sp<SkColorFilter> mColorFilter; + sk_sp<SkImageFilter> mImageFilter; + StretchEffect mStretchEffect; }; /* * Data structure that holds the properties for a RenderNode */ -class ANDROID_API RenderProperties { +class RenderProperties { public: RenderProperties(); virtual ~RenderProperties(); @@ -541,6 +553,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/SaveFlags.h b/libs/hwui/SaveFlags.h new file mode 100644 index 000000000000..f3579a8e9f19 --- /dev/null +++ b/libs/hwui/SaveFlags.h @@ -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. + */ + +#pragma once + +#include <inttypes.h> + +// TODO: Move this to an enum class +namespace android::SaveFlags { + +// These must match the corresponding Canvas API constants. +enum { + Matrix = 0x01, + Clip = 0x02, + HasAlphaLayer = 0x04, + ClipToLayer = 0x10, + + // Helper constant + MatrixClip = Matrix | Clip, +}; +typedef uint32_t Flags; + +} // namespace android::SaveFlags diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp index 941437998838..8fddf713f1fa 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> @@ -159,32 +160,20 @@ void SkiaCanvas::restoreToCount(int restoreCount) { } } -static inline SkCanvas::SaveLayerFlags layerFlags(SaveFlags::Flags flags) { - SkCanvas::SaveLayerFlags layerFlags = 0; - - if (!(flags & SaveFlags::ClipToLayer)) { - layerFlags |= SkCanvasPriv::kDontClipToLayer_SaveLayerFlag; - } - - return layerFlags; -} - -int SkiaCanvas::saveLayer(float left, float top, float right, float bottom, const SkPaint* paint, - SaveFlags::Flags flags) { +int SkiaCanvas::saveLayer(float left, float top, float right, float bottom, const SkPaint* paint) { const SkRect bounds = SkRect::MakeLTRB(left, top, right, bottom); - const SkCanvas::SaveLayerRec rec(&bounds, paint, layerFlags(flags)); + const SkCanvas::SaveLayerRec rec(&bounds, paint); return mCanvas->saveLayer(rec); } -int SkiaCanvas::saveLayerAlpha(float left, float top, float right, float bottom, int alpha, - SaveFlags::Flags flags) { +int SkiaCanvas::saveLayerAlpha(float left, float top, float right, float bottom, int alpha) { if (static_cast<unsigned>(alpha) < 0xFF) { SkPaint alphaPaint; alphaPaint.setAlpha(alpha); - return this->saveLayer(left, top, right, bottom, &alphaPaint, flags); + return this->saveLayer(left, top, right, bottom, &alphaPaint); } - return this->saveLayer(left, top, right, bottom, nullptr, flags); + return this->saveLayer(left, top, right, bottom, nullptr); } int SkiaCanvas::saveUnclippedLayer(int left, int top, int right, int bottom) { @@ -576,7 +565,8 @@ void SkiaCanvas::drawVertices(const SkVertices* vertices, SkBlendMode mode, cons void SkiaCanvas::drawBitmap(Bitmap& bitmap, float left, float top, const Paint* paint) { auto image = bitmap.makeImage(); apply_looper(paint, [&](const SkPaint& p) { - mCanvas->drawImage(image, left, top, &p); + auto sampling = SkSamplingOptions(p.getFilterQuality()); + mCanvas->drawImage(image, left, top, sampling, &p); }); } @@ -585,7 +575,8 @@ void SkiaCanvas::drawBitmap(Bitmap& bitmap, const SkMatrix& matrix, const Paint* SkAutoCanvasRestore acr(mCanvas, true); mCanvas->concat(matrix); apply_looper(paint, [&](const SkPaint& p) { - mCanvas->drawImage(image, 0, 0, &p); + auto sampling = SkSamplingOptions(p.getFilterQuality()); + mCanvas->drawImage(image, 0, 0, sampling, &p); }); } @@ -597,10 +588,17 @@ void SkiaCanvas::drawBitmap(Bitmap& bitmap, float srcLeft, float srcTop, float s SkRect dstRect = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom); apply_looper(paint, [&](const SkPaint& p) { - mCanvas->drawImageRect(image, srcRect, dstRect, &p, SkCanvas::kFast_SrcRectConstraint); + auto sampling = SkSamplingOptions(p.getFilterQuality()); + mCanvas->drawImageRect(image, srcRect, dstRect, sampling, &p, + SkCanvas::kFast_SrcRectConstraint); }); } +static SkFilterMode paintToFilter(const Paint* paint) { + return paint && paint->isFilterBitmap() ? SkFilterMode::kLinear + : SkFilterMode::kNearest; +} + void SkiaCanvas::drawBitmapMesh(Bitmap& bitmap, int meshWidth, int meshHeight, const float* vertices, const int* colors, const Paint* paint) { const int ptCount = (meshWidth + 1) * (meshHeight + 1); @@ -675,15 +673,25 @@ void SkiaCanvas::drawBitmapMesh(Bitmap& bitmap, int meshWidth, int meshHeight, } #endif + auto image = bitmap.makeImage(); + // cons-up a shader for the bitmap Paint pnt; if (paint) { pnt = *paint; } - pnt.setShader(bitmap.makeImage()->makeShader()); + SkSamplingOptions sampling(paintToFilter(&pnt)); + pnt.setShader(image->makeShader(sampling)); + auto v = builder.detach(); apply_looper(&pnt, [&](const SkPaint& p) { - mCanvas->drawVertices(v, SkBlendMode::kModulate, p); + SkPaint copy(p); + auto s = SkSamplingOptions(p.getFilterQuality()); + if (s != sampling) { + // apply_looper changed the quality? + copy.setShader(image->makeShader(s)); + } + mCanvas->drawVertices(v, SkBlendMode::kModulate, copy); }); } @@ -712,7 +720,8 @@ void SkiaCanvas::drawNinePatch(Bitmap& bitmap, const Res_png_9patch& chunk, floa SkRect dst = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom); auto image = bitmap.makeImage(); apply_looper(paint, [&](const SkPaint& p) { - mCanvas->drawImageLattice(image.get(), lattice, dst, &p); + auto filter = SkSamplingOptions(p.getFilterQuality()).filter; + mCanvas->drawImageLattice(image.get(), lattice, dst, filter, &p); }); } @@ -729,8 +738,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) { @@ -823,6 +831,18 @@ void SkiaCanvas::drawCircle(uirenderer::CanvasPropertyPrimitive* x, mCanvas->drawDrawable(drawable.get()); } +void SkiaCanvas::drawRipple(uirenderer::CanvasPropertyPrimitive* x, + uirenderer::CanvasPropertyPrimitive* y, + uirenderer::CanvasPropertyPrimitive* radius, + uirenderer::CanvasPropertyPaint* paint, + uirenderer::CanvasPropertyPrimitive* progress, + const SkRuntimeShaderBuilder& effectBuilder) { + sk_sp<uirenderer::skiapipeline::AnimatedRipple> drawable( + new uirenderer::skiapipeline::AnimatedRipple(x, y, radius, paint, progress, + effectBuilder)); + mCanvas->drawDrawable(drawable.get()); +} + void SkiaCanvas::drawPicture(const SkPicture& picture) { // TODO: Change to mCanvas->drawPicture()? SkCanvas::drawPicture seems to be // where the logic is for playback vs. ref picture. Using picture.playback here @@ -842,9 +862,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..155f6df7b703 100644 --- a/libs/hwui/SkiaCanvas.h +++ b/libs/hwui/SkiaCanvas.h @@ -53,12 +53,11 @@ public: LOG_ALWAYS_FATAL("SkiaCanvas cannot be reset as a recording canvas"); } - virtual uirenderer::DisplayList* finishRecording() override { + virtual void finishRecording(uirenderer::RenderNode*) override { 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; @@ -73,10 +72,8 @@ public: virtual void restoreToCount(int saveCount) override; virtual void restoreUnclippedLayer(int saveCount, const SkPaint& paint) override; - virtual int saveLayer(float left, float top, float right, float bottom, const SkPaint* paint, - SaveFlags::Flags flags) override; - virtual int saveLayerAlpha(float left, float top, float right, float bottom, int alpha, - SaveFlags::Flags flags) override; + virtual int saveLayer(float left, float top, float right, float bottom, const SkPaint* paint) override; + virtual int saveLayerAlpha(float left, float top, float right, float bottom, int alpha) override; virtual int saveUnclippedLayer(int left, int top, int right, int bottom) override; virtual void getMatrix(SkMatrix* outMatrix) const override; @@ -149,11 +146,15 @@ public: uirenderer::CanvasPropertyPrimitive* y, uirenderer::CanvasPropertyPrimitive* radius, uirenderer::CanvasPropertyPaint* paint) override; + virtual void drawRipple(uirenderer::CanvasPropertyPrimitive* x, + uirenderer::CanvasPropertyPrimitive* y, + uirenderer::CanvasPropertyPrimitive* radius, + uirenderer::CanvasPropertyPaint* paint, + uirenderer::CanvasPropertyPrimitive* progress, + const SkRuntimeShaderBuilder& effectBuilder) override; 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 +164,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; @@ -195,7 +195,6 @@ protected: } operator const SkPaint*() const { return mPtr; } const SkPaint* operator->() const { assert(mPtr); return mPtr; } - const SkPaint& operator*() const { assert(mPtr); return *mPtr; } explicit operator bool() { return mPtr != nullptr; } private: const SkPaint* mPtr; diff --git a/libs/hwui/VectorDrawable.cpp b/libs/hwui/VectorDrawable.cpp index cd908354aea5..4a21ad6ab945 100644 --- a/libs/hwui/VectorDrawable.cpp +++ b/libs/hwui/VectorDrawable.cpp @@ -505,14 +505,14 @@ void Tree::draw(SkCanvas* canvas, const SkRect& bounds, const SkPaint& inPaint) SkPaint paint = inPaint; paint.setAlpha(mProperties.getRootAlpha() * 255); - Bitmap& bitmap = getBitmapUpdateIfDirty(); - SkBitmap skiaBitmap; - bitmap.getSkBitmap(&skiaBitmap); + sk_sp<SkImage> cachedBitmap = getBitmapUpdateIfDirty().makeImage(); + // HWUI always draws VD with bilinear filtering. + auto sampling = SkSamplingOptions(SkFilterMode::kLinear); int scaledWidth = SkScalarCeilToInt(mProperties.getScaledWidth()); int scaledHeight = SkScalarCeilToInt(mProperties.getScaledHeight()); - canvas->drawBitmapRect(skiaBitmap, SkRect::MakeWH(scaledWidth, scaledHeight), bounds, - &paint, SkCanvas::kFast_SrcRectConstraint); + canvas->drawImageRect(cachedBitmap, SkRect::MakeWH(scaledWidth, scaledHeight), bounds, + sampling, &paint, SkCanvas::kFast_SrcRectConstraint); } void Tree::updateBitmapCache(Bitmap& bitmap, bool useStagingData) { 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/WebViewFunctorManager.cpp b/libs/hwui/WebViewFunctorManager.cpp index 68541b4b31f0..671c66f11e32 100644 --- a/libs/hwui/WebViewFunctorManager.cpp +++ b/libs/hwui/WebViewFunctorManager.cpp @@ -26,6 +26,35 @@ namespace android::uirenderer { +namespace { +class ScopedCurrentFunctor { +public: + ScopedCurrentFunctor(WebViewFunctor* functor) { + ALOG_ASSERT(!sCurrentFunctor); + ALOG_ASSERT(functor); + sCurrentFunctor = functor; + } + ~ScopedCurrentFunctor() { + ALOG_ASSERT(sCurrentFunctor); + sCurrentFunctor = nullptr; + } + + static ASurfaceControl* getSurfaceControl() { + ALOG_ASSERT(sCurrentFunctor); + return sCurrentFunctor->getSurfaceControl(); + } + static void mergeTransaction(ASurfaceTransaction* transaction) { + ALOG_ASSERT(sCurrentFunctor); + sCurrentFunctor->mergeTransaction(transaction); + } + +private: + static WebViewFunctor* sCurrentFunctor; +}; + +WebViewFunctor* ScopedCurrentFunctor::sCurrentFunctor = nullptr; +} // namespace + RenderMode WebViewFunctor_queryPlatformRenderMode() { auto pipelineType = Properties::getRenderPipelineType(); switch (pipelineType) { @@ -83,7 +112,15 @@ void WebViewFunctor::drawGl(const DrawGlInfo& drawInfo) { if (!mHasContext) { mHasContext = true; } - mCallbacks.gles.draw(mFunctor, mData, drawInfo); + ScopedCurrentFunctor currentFunctor(this); + + WebViewOverlayData overlayParams = { + // TODO: + .overlaysMode = OverlaysMode::Disabled, + .getSurfaceControl = currentFunctor.getSurfaceControl, + .mergeTransaction = currentFunctor.mergeTransaction, + }; + mCallbacks.gles.draw(mFunctor, mData, drawInfo, overlayParams); } void WebViewFunctor::initVk(const VkFunctorInitParams& params) { @@ -98,7 +135,15 @@ void WebViewFunctor::initVk(const VkFunctorInitParams& params) { void WebViewFunctor::drawVk(const VkFunctorDrawParams& params) { ATRACE_NAME("WebViewFunctor::drawVk"); - mCallbacks.vk.draw(mFunctor, mData, params); + ScopedCurrentFunctor currentFunctor(this); + + WebViewOverlayData overlayParams = { + // TODO + .overlaysMode = OverlaysMode::Disabled, + .getSurfaceControl = currentFunctor.getSurfaceControl, + .mergeTransaction = currentFunctor.mergeTransaction, + }; + mCallbacks.vk.draw(mFunctor, mData, params, overlayParams); } void WebViewFunctor::postDrawVk() { @@ -118,6 +163,20 @@ void WebViewFunctor::destroyContext() { } } +void WebViewFunctor::removeOverlays() { + ScopedCurrentFunctor currentFunctor(this); + mCallbacks.removeOverlays(mFunctor, mData, currentFunctor.mergeTransaction); +} + +ASurfaceControl* WebViewFunctor::getSurfaceControl() { + // TODO + return nullptr; +} + +void WebViewFunctor::mergeTransaction(ASurfaceTransaction* transaction) { + // TODO +} + WebViewFunctorManager& WebViewFunctorManager::instance() { static WebViewFunctorManager sInstance; return sInstance; diff --git a/libs/hwui/WebViewFunctorManager.h b/libs/hwui/WebViewFunctorManager.h index 675b738c6406..17b936ade45c 100644 --- a/libs/hwui/WebViewFunctorManager.h +++ b/libs/hwui/WebViewFunctorManager.h @@ -19,11 +19,11 @@ #include <private/hwui/WebViewFunctor.h> #ifdef __ANDROID__ // Layoutlib does not support render thread #include <renderthread/RenderProxy.h> -#else -#include <utils/Log.h> #endif #include <utils/LightRefBase.h> +#include <utils/Log.h> +#include <utils/StrongPointer.h> #include <mutex> #include <vector> @@ -56,6 +56,8 @@ public: void postDrawVk() { mReference.postDrawVk(); } + void removeOverlays() { mReference.removeOverlays(); } + private: friend class WebViewFunctor; @@ -71,6 +73,10 @@ public: void drawVk(const VkFunctorDrawParams& params); void postDrawVk(); void destroyContext(); + void removeOverlays(); + + ASurfaceControl* getSurfaceControl(); + void mergeTransaction(ASurfaceTransaction* transaction); sp<Handle> createHandle() { LOG_ALWAYS_FATAL_IF(mCreatedHandle); 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..0fad2d58cc8a 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,22 +62,24 @@ 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); extern int register_android_graphics_drawable_VectorDrawable(JNIEnv* env); extern int register_android_graphics_fonts_Font(JNIEnv* env); extern int register_android_graphics_fonts_FontFamily(JNIEnv* env); +extern int register_android_graphics_fonts_NativeFont(JNIEnv* env); extern int register_android_graphics_pdf_PdfDocument(JNIEnv* env); 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 +126,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), @@ -131,16 +136,17 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_graphics_drawable_VectorDrawable), REG_JNI(register_android_graphics_fonts_Font), REG_JNI(register_android_graphics_fonts_FontFamily), + REG_JNI(register_android_graphics_fonts_NativeFont), REG_JNI(register_android_graphics_pdf_PdfDocument), REG_JNI(register_android_graphics_pdf_PdfEditor), 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 +178,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/canvas/CanvasFrontend.cpp b/libs/hwui/canvas/CanvasFrontend.cpp new file mode 100644 index 000000000000..8f261c83b8d3 --- /dev/null +++ b/libs/hwui/canvas/CanvasFrontend.cpp @@ -0,0 +1,124 @@ +/* + * 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 "CanvasFrontend.h" +#include "CanvasOps.h" +#include "CanvasOpBuffer.h" + +namespace android::uirenderer { + +CanvasStateHelper::CanvasStateHelper(int width, int height) { + resetState(width, height); +} + +void CanvasStateHelper::resetState(int width, int height) { + mInitialBounds = SkIRect::MakeWH(width, height); + mSaveStack.clear(); + mClipStack.clear(); + mTransformStack.clear(); + mSaveStack.emplace_back(); + mClipStack.emplace_back().setRect(mInitialBounds); + mTransformStack.emplace_back(); + mCurrentClipIndex = 0; + mCurrentTransformIndex = 0; +} + +bool CanvasStateHelper::internalSave(SaveEntry saveEntry) { + mSaveStack.push_back(saveEntry); + if (saveEntry.matrix) { + // We need to push before accessing transform() to ensure the reference doesn't move + // across vector resizes + mTransformStack.emplace_back() = transform(); + mCurrentTransformIndex += 1; + } + if (saveEntry.clip) { + // We need to push before accessing clip() to ensure the reference doesn't move + // across vector resizes + mClipStack.emplace_back() = clip(); + mCurrentClipIndex += 1; + return true; + } + return false; +} + +// Assert that the cast from SkClipOp to SkRegion::Op is valid +static_assert(static_cast<int>(SkClipOp::kDifference) == SkRegion::Op::kDifference_Op); +static_assert(static_cast<int>(SkClipOp::kIntersect) == SkRegion::Op::kIntersect_Op); +static_assert(static_cast<int>(SkClipOp::kUnion_deprecated) == SkRegion::Op::kUnion_Op); +static_assert(static_cast<int>(SkClipOp::kXOR_deprecated) == SkRegion::Op::kXOR_Op); +static_assert(static_cast<int>(SkClipOp::kReverseDifference_deprecated) == SkRegion::Op::kReverseDifference_Op); +static_assert(static_cast<int>(SkClipOp::kReplace_deprecated) == SkRegion::Op::kReplace_Op); + +void CanvasStateHelper::internalClipRect(const SkRect& rect, SkClipOp op) { + clip().opRect(rect, transform(), mInitialBounds, (SkRegion::Op)op, false); +} + +void CanvasStateHelper::internalClipPath(const SkPath& path, SkClipOp op) { + clip().opPath(path, transform(), mInitialBounds, (SkRegion::Op)op, true); +} + +bool CanvasStateHelper::internalRestore() { + // Prevent underflows + if (saveCount() <= 1) { + return false; + } + + SaveEntry entry = mSaveStack[mSaveStack.size() - 1]; + mSaveStack.pop_back(); + bool needsRestorePropagation = entry.layer; + if (entry.matrix) { + mTransformStack.pop_back(); + mCurrentTransformIndex -= 1; + } + if (entry.clip) { + // We need to push before accessing clip() to ensure the reference doesn't move + // across vector resizes + mClipStack.pop_back(); + mCurrentClipIndex -= 1; + needsRestorePropagation = true; + } + return needsRestorePropagation; +} + +SkRect CanvasStateHelper::getClipBounds() const { + SkIRect ibounds = clip().getBounds(); + + if (ibounds.isEmpty()) { + return SkRect::MakeEmpty(); + } + + SkMatrix inverse; + // if we can't invert the CTM, we can't return local clip bounds + if (!transform().invert(&inverse)) { + return SkRect::MakeEmpty(); + } + + SkRect ret = SkRect::MakeEmpty(); + inverse.mapRect(&ret, SkRect::Make(ibounds)); + return ret; +} + +bool CanvasStateHelper::quickRejectRect(float left, float top, float right, float bottom) const { + // TODO: Implement + return false; +} + +bool CanvasStateHelper::quickRejectPath(const SkPath& path) const { + // TODO: Implement + return false; +} + +} // namespace android::uirenderer diff --git a/libs/hwui/canvas/CanvasFrontend.h b/libs/hwui/canvas/CanvasFrontend.h new file mode 100644 index 000000000000..d749d2f2596b --- /dev/null +++ b/libs/hwui/canvas/CanvasFrontend.h @@ -0,0 +1,212 @@ +/* + * 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 + +// TODO: Can we get the dependencies scoped down more? +#include "CanvasOps.h" +#include "CanvasOpBuffer.h" +#include <SaveFlags.h> + +#include <SkRasterClip.h> +#include <ui/FatVector.h> + +#include <optional> + +namespace android::uirenderer { + +// Exists to avoid forcing all this common logic into the templated class +class CanvasStateHelper { +protected: + CanvasStateHelper(int width, int height); + ~CanvasStateHelper() = default; + + struct SaveEntry { + bool clip : 1 = false; + bool matrix : 1 = false; + bool layer : 1 = false; + }; + + constexpr SaveEntry saveEntryForLayer() { + return { + .clip = true, + .matrix = true, + .layer = true, + }; + } + + constexpr SaveEntry flagsToSaveEntry(SaveFlags::Flags flags) { + return SaveEntry { + .clip = static_cast<bool>(flags & SaveFlags::Clip), + .matrix = static_cast<bool>(flags & SaveFlags::Matrix), + .layer = false + }; + } + + bool internalSave(SaveEntry saveEntry); + + void internalSaveLayer(const SkCanvas::SaveLayerRec& layerRec) { + internalSave({ + .clip = true, + .matrix = true, + .layer = true + }); + internalClipRect(*layerRec.fBounds, SkClipOp::kIntersect); + } + + bool internalRestore(); + + void internalClipRect(const SkRect& rect, SkClipOp op); + void internalClipPath(const SkPath& path, SkClipOp op); + + SkIRect mInitialBounds; + FatVector<SaveEntry, 6> mSaveStack; + FatVector<SkMatrix, 6> mTransformStack; + FatVector<SkConservativeClip, 6> mClipStack; + + size_t mCurrentTransformIndex; + size_t mCurrentClipIndex; + + const SkConservativeClip& clip() const { + return mClipStack[mCurrentClipIndex]; + } + + SkConservativeClip& clip() { + return mClipStack[mCurrentClipIndex]; + } + + void resetState(int width, int height); + +public: + int saveCount() const { return mSaveStack.size(); } + + SkRect getClipBounds() const; + bool quickRejectRect(float left, float top, float right, float bottom) const; + bool quickRejectPath(const SkPath& path) const; + + const SkMatrix& transform() const { + return mTransformStack[mCurrentTransformIndex]; + } + + SkMatrix& transform() { + return mTransformStack[mCurrentTransformIndex]; + } + + // For compat with existing HWUI Canvas interface + void getMatrix(SkMatrix* outMatrix) const { + *outMatrix = transform(); + } + + void setMatrix(const SkMatrix& matrix) { + transform() = matrix; + } + + void concat(const SkMatrix& matrix) { + transform().preConcat(matrix); + } + + void rotate(float degrees) { + SkMatrix m; + m.setRotate(degrees); + concat(m); + } + + void scale(float sx, float sy) { + SkMatrix m; + m.setScale(sx, sy); + concat(m); + } + + void skew(float sx, float sy) { + SkMatrix m; + m.setSkew(sx, sy); + concat(m); + } + + void translate(float dx, float dy) { + transform().preTranslate(dx, dy); + } +}; + +// Front-end canvas that handles queries, up-front state, and produces CanvasOp<> output downstream +template <typename CanvasOpReceiver> +class CanvasFrontend final : public CanvasStateHelper { +public: + template<class... Args> + CanvasFrontend(int width, int height, Args&&... args) : CanvasStateHelper(width, height), + mReceiver(std::forward<Args>(args)...) { } + ~CanvasFrontend() = default; + + void save(SaveFlags::Flags flags = SaveFlags::MatrixClip) { + if (internalSave(flagsToSaveEntry(flags))) { + submit<CanvasOpType::Save>({}); + } + } + + void restore() { + if (internalRestore()) { + submit<CanvasOpType::Restore>({}); + } + } + + template <CanvasOpType T> + void draw(CanvasOp<T>&& op) { + // The front-end requires going through certain front-doors, which these aren't. + static_assert(T != CanvasOpType::Save, "Must use CanvasFrontend::save() call instead"); + static_assert(T != CanvasOpType::Restore, "Must use CanvasFrontend::restore() call instead"); + + if constexpr (T == CanvasOpType::SaveLayer) { + internalSaveLayer(op.saveLayerRec); + } + if constexpr (T == CanvasOpType::SaveBehind) { + // Don't use internalSaveLayer as this doesn't apply clipping, it's a "regular" save + // But we do want to flag it as a layer, such that restore is Definitely Required + internalSave(saveEntryForLayer()); + } + if constexpr (T == CanvasOpType::ClipRect) { + internalClipRect(op.rect, op.op); + } + if constexpr (T == CanvasOpType::ClipPath) { + internalClipPath(op.path, op.op); + } + + submit(std::move(op)); + } + + const CanvasOpReceiver& receiver() const { return *mReceiver; } + + CanvasOpReceiver finish() { + auto ret = std::move(mReceiver.value()); + mReceiver.reset(); + return std::move(ret); + } + + template<class... Args> + void reset(int newWidth, int newHeight, Args&&... args) { + resetState(newWidth, newHeight); + mReceiver.emplace(std::forward<Args>(args)...); + } + +private: + std::optional<CanvasOpReceiver> mReceiver; + + template <CanvasOpType T> + void submit(CanvasOp<T>&& op) { + mReceiver->push_container(CanvasOpContainer(std::move(op), transform())); + } +}; + +} // namespace android::uirenderer diff --git a/libs/hwui/canvas/CanvasOpBuffer.cpp b/libs/hwui/canvas/CanvasOpBuffer.cpp new file mode 100644 index 000000000000..7054e47eac89 --- /dev/null +++ b/libs/hwui/canvas/CanvasOpBuffer.cpp @@ -0,0 +1,25 @@ +/* + * 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 "CanvasOpBuffer.h" + +#include "CanvasOps.h" + +namespace android::uirenderer { + +template class OpBuffer<CanvasOpType, CanvasOpContainer>; + +} // namespace android::uirenderer diff --git a/libs/hwui/canvas/CanvasOpBuffer.h b/libs/hwui/canvas/CanvasOpBuffer.h new file mode 100644 index 000000000000..07e079a7d57f --- /dev/null +++ b/libs/hwui/canvas/CanvasOpBuffer.h @@ -0,0 +1,64 @@ +/* + * 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 <SkMatrix.h> + +#include "CanvasOpTypes.h" +#include "OpBuffer.h" + +namespace android::uirenderer { + +template <CanvasOpType T> +struct CanvasOp; + +template <CanvasOpType T> +class CanvasOpContainer { +private: + BE_OPBUFFERS_FRIEND(); + + OpBufferItemHeader<CanvasOpType> header; + // TODO: Figure out some magic to make this not be here when it's identity (or not used) + SkMatrix mTransform; + CanvasOp<T> mImpl; + +public: + CanvasOpContainer(CanvasOp<T>&& impl, const SkMatrix& transform = SkMatrix::I()) + : mTransform(transform), mImpl(std::move(impl)) {} + + uint32_t size() const { return header.size; } + CanvasOpType type() const { return header.type; } + + const SkMatrix& transform() const { return mTransform; } + + CanvasOp<T>* operator->() noexcept { return &mImpl; } + const CanvasOp<T>* operator->() const noexcept { return &mImpl; } + + CanvasOp<T>& op() noexcept { return mImpl; } + const CanvasOp<T>& op() const noexcept { return mImpl; } +}; + +extern template class OpBuffer<CanvasOpType, CanvasOpContainer>; +class CanvasOpBuffer final : public OpBuffer<CanvasOpType, CanvasOpContainer> { +public: + template <CanvasOpType T> + void push(CanvasOp<T>&& op) { + push_container(CanvasOpContainer<T>(std::move(op))); + } +}; + +} // namespace android::uirenderer diff --git a/libs/hwui/canvas/CanvasOpRasterizer.cpp b/libs/hwui/canvas/CanvasOpRasterizer.cpp new file mode 100644 index 000000000000..0093c38cf8a8 --- /dev/null +++ b/libs/hwui/canvas/CanvasOpRasterizer.cpp @@ -0,0 +1,54 @@ +/* + * 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 "CanvasOpRasterizer.h" + +#include <SkCanvas.h> +#include <log/log.h> + +#include <vector> + +#include "CanvasOpBuffer.h" +#include "CanvasOps.h" + +namespace android::uirenderer { + +void rasterizeCanvasBuffer(const CanvasOpBuffer& source, SkCanvas* destination) { + // Tracks the global transform from the current display list back toward the display space + // Push on beginning a RenderNode draw, pop on ending one + std::vector<SkMatrix> globalMatrixStack; + SkMatrix& currentGlobalTransform = globalMatrixStack.emplace_back(SkMatrix::I()); + + source.for_each([&]<CanvasOpType T>(const CanvasOpContainer<T> * op) { + if constexpr ( + T == CanvasOpType::BeginZ || + T == CanvasOpType::EndZ || + T == CanvasOpType::DrawLayer + ) { + // Do beginZ or endZ + LOG_ALWAYS_FATAL("TODO"); + return; + } else { + // Generic OP + // First apply the current transformation + destination->setMatrix(SkMatrix::Concat(currentGlobalTransform, op->transform())); + // Now draw it + (*op)->draw(destination); + } + }); +} + +} // namespace android::uirenderer diff --git a/libs/hwui/canvas/CanvasOpRasterizer.h b/libs/hwui/canvas/CanvasOpRasterizer.h new file mode 100644 index 000000000000..c2235ab84d56 --- /dev/null +++ b/libs/hwui/canvas/CanvasOpRasterizer.h @@ -0,0 +1,64 @@ +/* + * 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 <hwui/Bitmap.h> + +#include <SkBitmap.h> +#include <SkCanvas.h> + +#include "CanvasOps.h" + +#include <experimental/type_traits> +#include <variant> + +namespace android::uirenderer { + +class CanvasOpBuffer; + +void rasterizeCanvasBuffer(const CanvasOpBuffer& source, SkCanvas* destination); + +class ImmediateModeRasterizer { +public: + explicit ImmediateModeRasterizer(std::unique_ptr<SkCanvas>&& canvas) { + mCanvas = canvas.get(); + mOwnership = std::move(canvas); + } + + explicit ImmediateModeRasterizer(std::shared_ptr<SkCanvas> canvas) { + mCanvas = canvas.get(); + mOwnership = std::move(canvas); + } + + explicit ImmediateModeRasterizer(Bitmap& bitmap) { + mCanvas = &(mOwnership.emplace<SkCanvas>(bitmap.getSkBitmap())); + } + + template <CanvasOpType T> + void draw(const CanvasOp<T>& op) { + if constexpr (CanvasOpTraits::can_draw<CanvasOp<T>>) { + op.draw(mCanvas); + } + } + +private: + SkCanvas* mCanvas; + // Just here to keep mCanvas alive. Thankfully we never need to actually look inside this... + std::variant<SkCanvas, std::shared_ptr<SkCanvas>, std::unique_ptr<SkCanvas>> mOwnership; +}; + +} // namespace android::uirenderer diff --git a/libs/hwui/canvas/CanvasOpRecorder.cpp b/libs/hwui/canvas/CanvasOpRecorder.cpp new file mode 100644 index 000000000000..bb968ee84670 --- /dev/null +++ b/libs/hwui/canvas/CanvasOpRecorder.cpp @@ -0,0 +1,22 @@ +/* + * 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 "CanvasOpRecorder.h" + +#include "CanvasOpBuffer.h" +#include "CanvasOps.h" + +namespace android::uirenderer {} // namespace android::uirenderer diff --git a/libs/hwui/canvas/CanvasOpRecorder.h b/libs/hwui/canvas/CanvasOpRecorder.h new file mode 100644 index 000000000000..7d95bc4785ea --- /dev/null +++ b/libs/hwui/canvas/CanvasOpRecorder.h @@ -0,0 +1,38 @@ +/* + * 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 "hwui/Canvas.h" +#include "CanvasOpBuffer.h" + +#include <vector> + +namespace android::uirenderer { + +// Interop with existing HWUI Canvas +class CanvasOpRecorder final : /* todo: public Canvas */ { +public: + // Transform ops +private: + struct SaveEntry { + + }; + + std::vector<SaveEntry> mSaveStack; +}; + +} // namespace android::uirenderer diff --git a/libs/hwui/canvas/CanvasOpTypes.h b/libs/hwui/canvas/CanvasOpTypes.h new file mode 100644 index 000000000000..cde50bd66a15 --- /dev/null +++ b/libs/hwui/canvas/CanvasOpTypes.h @@ -0,0 +1,68 @@ +/* + * 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 <inttypes.h> + +namespace android::uirenderer { + +enum class CanvasOpType : int8_t { + // State ops + // TODO: Eliminate the end ops by having the start include the end-at position + Save, + SaveLayer, + SaveBehind, + Restore, + BeginZ, + EndZ, + + // Clip ops + ClipRect, + ClipPath, + + // Drawing ops + DrawColor, + DrawRect, + DrawRegion, + DrawRoundRect, + DrawRoundRectProperty, + DrawDoubleRoundRect, + DrawCircleProperty, + DrawRippleProperty, + DrawCircle, + DrawOval, + DrawArc, + DrawPaint, + DrawPoint, + DrawPoints, + DrawPath, + DrawLine, + DrawLines, + DrawVertices, + DrawImage, + DrawImageRect, + // DrawImageLattice also used to draw 9 patches + DrawImageLattice, + DrawPicture, + DrawLayer, + + // TODO: Rest + + COUNT // must be last +}; + +} // namespace android::uirenderer
\ No newline at end of file diff --git a/libs/hwui/canvas/CanvasOps.h b/libs/hwui/canvas/CanvasOps.h new file mode 100644 index 000000000000..86b1ac71f692 --- /dev/null +++ b/libs/hwui/canvas/CanvasOps.h @@ -0,0 +1,456 @@ +/* + * 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 <SkAndroidFrameworkUtils.h> +#include <SkCanvas.h> +#include <SkPath.h> +#include <SkRegion.h> +#include <SkVertices.h> +#include <SkImage.h> +#include <SkPicture.h> +#include <SkRuntimeEffect.h> +#include <hwui/Bitmap.h> +#include <log/log.h> +#include "CanvasProperty.h" +#include "Points.h" + +#include "CanvasOpTypes.h" +#include "Layer.h" + +#include <experimental/type_traits> +#include <utility> + +namespace android::uirenderer { + +template <CanvasOpType T> +struct CanvasOp; + +struct CanvasOpTraits { + CanvasOpTraits() = delete; + + template<class T> + using draw_t = decltype(std::integral_constant<void (T::*)(SkCanvas*) const, &T::draw>{}); + + template <class T> + static constexpr bool can_draw = std::experimental::is_detected_v<draw_t, T>; +}; + +#define ASSERT_DRAWABLE() private: constexpr void _check_drawable() \ + { static_assert(CanvasOpTraits::can_draw<std::decay_t<decltype(*this)>>); } + +// ---------------------------------------------- +// State Ops +// --------------------------------------------- + +template <> +struct CanvasOp<CanvasOpType::Save> { + void draw(SkCanvas* canvas) const { canvas->save(); } + ASSERT_DRAWABLE() +}; + +template <> +struct CanvasOp<CanvasOpType::SaveLayer> { + SkCanvas::SaveLayerRec saveLayerRec; + void draw(SkCanvas* canvas) const { canvas->saveLayer(saveLayerRec); } + ASSERT_DRAWABLE() +}; + +template <> +struct CanvasOp<CanvasOpType::SaveBehind> { + SkRect bounds; + void draw(SkCanvas* canvas) const { SkAndroidFrameworkUtils::SaveBehind(canvas, &bounds); } + ASSERT_DRAWABLE() +}; + +template <> +struct CanvasOp<CanvasOpType::Restore> { + void draw(SkCanvas* canvas) const { canvas->restore(); } + ASSERT_DRAWABLE() +}; + +template <> +struct CanvasOp<CanvasOpType::BeginZ> { +}; +template <> +struct CanvasOp<CanvasOpType::EndZ> {}; + +// ---------------------------------------------- +// Clip Ops +// --------------------------------------------- + +template <> +struct CanvasOp<CanvasOpType::ClipRect> { + SkRect rect; + SkClipOp clipOp; + void draw(SkCanvas* canvas) const { canvas->clipRect(rect, clipOp); } + ASSERT_DRAWABLE() +}; + +template <> +struct CanvasOp<CanvasOpType::ClipPath> { + SkPath path; + SkClipOp op; + void draw(SkCanvas* canvas) const { canvas->clipPath(path, op, true); } + ASSERT_DRAWABLE() +}; + +// ---------------------------------------------- +// Drawing Ops +// --------------------------------------------- + +template<> +struct CanvasOp<CanvasOpType::DrawRoundRectProperty> { + sp<uirenderer::CanvasPropertyPrimitive> left; + sp<uirenderer::CanvasPropertyPrimitive> top; + sp<uirenderer::CanvasPropertyPrimitive> right; + sp<uirenderer::CanvasPropertyPrimitive> bottom; + sp<uirenderer::CanvasPropertyPrimitive> rx; + sp<uirenderer::CanvasPropertyPrimitive> ry; + sp<uirenderer::CanvasPropertyPaint> paint; + + void draw(SkCanvas* canvas) const { + SkRect rect = SkRect::MakeLTRB(left->value, top->value, right->value, bottom->value); + canvas->drawRoundRect(rect, rx->value, ry->value, paint->value); + } + ASSERT_DRAWABLE() +}; + +template<> +struct CanvasOp<CanvasOpType::DrawCircleProperty> { + sp<uirenderer::CanvasPropertyPrimitive> x; + sp<uirenderer::CanvasPropertyPrimitive> y; + sp<uirenderer::CanvasPropertyPrimitive> radius; + sp<uirenderer::CanvasPropertyPaint> paint; + + void draw(SkCanvas* canvas) const { + canvas->drawCircle(x->value, y->value, radius->value, paint->value); + } + ASSERT_DRAWABLE() +}; + +template<> +struct CanvasOp<CanvasOpType::DrawRippleProperty> { + sp<uirenderer::CanvasPropertyPrimitive> x; + sp<uirenderer::CanvasPropertyPrimitive> y; + sp<uirenderer::CanvasPropertyPrimitive> radius; + sp<uirenderer::CanvasPropertyPaint> paint; + sp<uirenderer::CanvasPropertyPrimitive> progress; + sk_sp<SkRuntimeEffect> effect; + + void draw(SkCanvas* canvas) const { + SkRuntimeShaderBuilder runtimeEffectBuilder(effect); + + SkRuntimeShaderBuilder::BuilderUniform center = runtimeEffectBuilder.uniform("in_origin"); + if (center.fVar != nullptr) { + center = SkV2{x->value, y->value}; + } + + SkRuntimeShaderBuilder::BuilderUniform radiusU = + runtimeEffectBuilder.uniform("in_radius"); + if (radiusU.fVar != nullptr) { + radiusU = radius->value; + } + + SkRuntimeShaderBuilder::BuilderUniform progressU = + runtimeEffectBuilder.uniform("in_progress"); + if (progressU.fVar != nullptr) { + progressU = progress->value; + } + + SkPaint paintMod = paint->value; + paintMod.setShader(runtimeEffectBuilder.makeShader(nullptr, false)); + canvas->drawCircle(x->value, y->value, radius->value, paintMod); + } + ASSERT_DRAWABLE() +}; + +template <> +struct CanvasOp<CanvasOpType::DrawColor> { + SkColor4f color; + SkBlendMode mode; + void draw(SkCanvas* canvas) const { canvas->drawColor(color, mode); } + ASSERT_DRAWABLE() +}; + +template <> +struct CanvasOp<CanvasOpType::DrawPaint> { + SkPaint paint; + void draw(SkCanvas* canvas) const { canvas->drawPaint(paint); } + ASSERT_DRAWABLE() +}; + +template <> +struct CanvasOp<CanvasOpType::DrawPoint> { + float x; + float y; + SkPaint paint; + void draw(SkCanvas* canvas) const { canvas->drawPoint(x, y, paint); } + ASSERT_DRAWABLE() +}; + +template <> +struct CanvasOp<CanvasOpType::DrawPoints> { + size_t count; + SkPaint paint; + sk_sp<Points> points; + void draw(SkCanvas* canvas) const { + canvas->drawPoints( + SkCanvas::kPoints_PointMode, + count, + points->data(), + paint + ); + } + ASSERT_DRAWABLE() +}; + +template <> +struct CanvasOp<CanvasOpType::DrawRect> { + SkRect rect; + SkPaint paint; + void draw(SkCanvas* canvas) const { canvas->drawRect(rect, paint); } + ASSERT_DRAWABLE() +}; + +template <> +struct CanvasOp<CanvasOpType::DrawRegion> { + SkRegion region; + SkPaint paint; + void draw(SkCanvas* canvas) const { canvas->drawRegion(region, paint); } + ASSERT_DRAWABLE() +}; + +template<> +struct CanvasOp<CanvasOpType::DrawRoundRect> { + SkRect rect; + SkScalar rx; + SkScalar ry; + SkPaint paint; + void draw(SkCanvas* canvas) const { + canvas->drawRoundRect(rect, rx, ry, paint); + } + ASSERT_DRAWABLE() +}; + +template<> +struct CanvasOp<CanvasOpType::DrawDoubleRoundRect> { + SkRRect outer; + SkRRect inner; + SkPaint paint; + void draw(SkCanvas* canvas) const { + canvas->drawDRRect(outer, inner, paint); + } + ASSERT_DRAWABLE() +}; + +template<> +struct CanvasOp<CanvasOpType::DrawCircle> { + SkScalar cx; + SkScalar cy; + SkScalar radius; + SkPaint paint; + void draw(SkCanvas* canvas) const { + canvas->drawCircle(cx, cy, radius, paint); + } + ASSERT_DRAWABLE() +}; + +template<> +struct CanvasOp<CanvasOpType::DrawOval> { + SkRect oval; + SkPaint paint; + void draw(SkCanvas* canvas) const { + canvas->drawOval(oval, paint); + } + ASSERT_DRAWABLE() +}; + +template<> +struct CanvasOp<CanvasOpType::DrawArc> { + SkRect oval; + SkScalar startAngle; + SkScalar sweepAngle; + bool useCenter; + SkPaint paint; + + void draw(SkCanvas* canvas) const { + canvas->drawArc(oval, startAngle, sweepAngle, useCenter, paint); + } + ASSERT_DRAWABLE() +}; + +template<> +struct CanvasOp<CanvasOpType::DrawPath> { + SkPath path; + SkPaint paint; + + void draw(SkCanvas* canvas) const { canvas->drawPath(path, paint); } + ASSERT_DRAWABLE() +}; + +template<> +struct CanvasOp<CanvasOpType::DrawLine> { + float startX; + float startY; + float endX; + float endY; + SkPaint paint; + + void draw(SkCanvas* canvas) const { + canvas->drawLine(startX, startY, endX, endY, paint); + } + ASSERT_DRAWABLE() +}; + +template<> +struct CanvasOp<CanvasOpType::DrawLines> { + size_t count; + SkPaint paint; + sk_sp<Points> points; + void draw(SkCanvas* canvas) const { + canvas->drawPoints( + SkCanvas::kLines_PointMode, + count, + points->data(), + paint + ); + } + ASSERT_DRAWABLE() +}; + +template<> +struct CanvasOp<CanvasOpType::DrawVertices> { + sk_sp<SkVertices> vertices; + SkBlendMode mode; + SkPaint paint; + void draw(SkCanvas* canvas) const { + canvas->drawVertices(vertices, mode, paint); + } + ASSERT_DRAWABLE() +}; + +template<> +struct CanvasOp<CanvasOpType::DrawImage> { + + CanvasOp( + const sk_sp<Bitmap>& bitmap, + float left, + float top, + SkFilterMode filter, + SkPaint paint + ) : left(left), + top(top), + filter(filter), + paint(std::move(paint)), + bitmap(bitmap), + image(bitmap->makeImage()) { } + + float left; + float top; + SkFilterMode filter; + SkPaint paint; + sk_sp<Bitmap> bitmap; + sk_sp<SkImage> image; + + void draw(SkCanvas* canvas) const { + canvas->drawImage(image, left, top, SkSamplingOptions(filter), &paint); + } + ASSERT_DRAWABLE() +}; + +template<> +struct CanvasOp<CanvasOpType::DrawImageRect> { + + CanvasOp( + const sk_sp<Bitmap>& bitmap, + SkRect src, + SkRect dst, + SkFilterMode filter, + SkPaint paint + ) : src(src), + dst(dst), + filter(filter), + paint(std::move(paint)), + bitmap(bitmap), + image(bitmap->makeImage()) { } + + SkRect src; + SkRect dst; + SkFilterMode filter; + SkPaint paint; + sk_sp<Bitmap> bitmap; + sk_sp<SkImage> image; + + void draw(SkCanvas* canvas) const { + canvas->drawImageRect(image, + src, + dst, + SkSamplingOptions(filter), + &paint, + SkCanvas::kFast_SrcRectConstraint + ); + } + ASSERT_DRAWABLE() +}; + +template<> +struct CanvasOp<CanvasOpType::DrawImageLattice> { + + CanvasOp( + const sk_sp<Bitmap>& bitmap, + SkRect dst, + SkCanvas::Lattice lattice, + SkFilterMode filter, + SkPaint paint + ): dst(dst), + lattice(lattice), + filter(filter), + bitmap(bitmap), + image(bitmap->makeImage()), + paint(std::move(paint)) {} + + SkRect dst; + SkCanvas::Lattice lattice; + SkFilterMode filter; + const sk_sp<Bitmap> bitmap; + const sk_sp<SkImage> image; + + SkPaint paint; + void draw(SkCanvas* canvas) const { + canvas->drawImageLattice(image.get(), lattice, dst, filter, &paint); + } + ASSERT_DRAWABLE() +}; + +template<> +struct CanvasOp<CanvasOpType::DrawPicture> { + sk_sp<SkPicture> picture; + void draw(SkCanvas* canvas) const { + picture->playback(canvas); + } +}; + +template<> +struct CanvasOp<CanvasOpType::DrawLayer> { + sp<Layer> layer; +}; + +// cleanup our macros +#undef ASSERT_DRAWABLE + +} // namespace android::uirenderer diff --git a/libs/hwui/canvas/OpBuffer.h b/libs/hwui/canvas/OpBuffer.h new file mode 100644 index 000000000000..1237d69a3c24 --- /dev/null +++ b/libs/hwui/canvas/OpBuffer.h @@ -0,0 +1,288 @@ +/* + * 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 <algorithm> +#include <array> +#include <cinttypes> +#include <cstddef> +#include <cstdlib> +#include <type_traits> +#include <utility> + +namespace android::uirenderer { + +template <typename T> +struct OpBufferItemHeader { + T type : 8; + uint32_t size : 24; +}; + +struct OpBufferAllocationHeader { + // Used size, including header size + size_t used = 0; + // Capacity, including header size + size_t capacity = 0; + // Offset relative to `this` at which the first item is + size_t startOffset = 0; + // Offset relative to `this` at which the last item is + size_t endOffset = 0; +}; + +#define BE_OPBUFFERS_FRIEND() \ + template <typename ItemTypes, template <ItemTypes> typename, typename, typename> \ + friend class OpBuffer + +template <typename ItemTypes, template <ItemTypes> typename ItemContainer, + typename BufferHeader = OpBufferAllocationHeader, + typename ItemTypesSequence = std::make_index_sequence<static_cast<int>(ItemTypes::COUNT)>> +class OpBuffer { + // Instead of re-aligning individual inserts, just pad the size of everything + // to a multiple of pointer alignment. This assumes we never work with doubles. + // Which we don't. + static constexpr size_t Alignment = alignof(void*); + + static constexpr size_t PadAlign(size_t size) { + return (size + (Alignment - 1)) & -Alignment; + } + +public: + static constexpr auto STARTING_SIZE = PadAlign(sizeof(BufferHeader)); + using ItemHeader = OpBufferItemHeader<ItemTypes>; + + OpBuffer() = default; + + // Prevent copying by default + OpBuffer(const OpBuffer&) = delete; + void operator=(const OpBuffer&) = delete; + + OpBuffer(OpBuffer&& other) { + mBuffer = other.mBuffer; + other.mBuffer = nullptr; + } + + void operator=(OpBuffer&& other) { + destroy(); + mBuffer = other.mBuffer; + other.mBuffer = nullptr; + } + + ~OpBuffer() { + destroy(); + } + + constexpr size_t capacity() const { return mBuffer ? mBuffer->capacity : 0; } + + constexpr size_t size() const { return mBuffer ? mBuffer->used : 0; } + + constexpr size_t remaining() const { return capacity() - size(); } + + // TODO: Add less-copy'ing variants of this. emplace_back? deferred initialization? + template <ItemTypes T> + void push_container(ItemContainer<T>&& op) { + static_assert(alignof(ItemContainer<T>) <= Alignment); + static_assert(offsetof(ItemContainer<T>, header) == 0); + + constexpr auto padded_size = PadAlign(sizeof(ItemContainer<T>)); + if (remaining() < padded_size) { + resize(std::max(padded_size, capacity()) * 2); + } + mBuffer->endOffset = mBuffer->used; + mBuffer->used += padded_size; + + void* allocateAt = reinterpret_cast<uint8_t*>(mBuffer) + mBuffer->endOffset; + auto temp = new (allocateAt) ItemContainer<T>{std::move(op)}; + temp->header = {.type = T, .size = padded_size}; + } + + void resize(size_t newsize) { + // Add the header size to newsize + const size_t adjustedSize = newsize + STARTING_SIZE; + + if (adjustedSize < size()) { + // todo: throw? + return; + } + if (newsize == 0) { + free(mBuffer); + mBuffer = nullptr; + } else { + if (mBuffer) { + mBuffer = reinterpret_cast<BufferHeader*>(realloc(mBuffer, adjustedSize)); + mBuffer->capacity = adjustedSize; + } else { + mBuffer = new (malloc(adjustedSize)) BufferHeader(); + mBuffer->capacity = adjustedSize; + mBuffer->used = STARTING_SIZE; + mBuffer->startOffset = STARTING_SIZE; + } + } + } + + template <typename F> + void for_each(F&& f) const { + for_each(std::forward<F>(f), ItemTypesSequence{}); + } + + void clear(); + + ItemHeader* first() const { return isEmpty() ? nullptr : itemAt(mBuffer->startOffset); } + + ItemHeader* last() const { return isEmpty() ? nullptr : itemAt(mBuffer->endOffset); } + + class sentinal { + public: + explicit sentinal(const uint8_t* end) : end(end) {} + private: + const uint8_t* const end; + }; + + sentinal end() const { + return sentinal{end_ptr()}; + } + + template <ItemTypes T> + class filtered_iterator { + public: + explicit filtered_iterator(uint8_t* start, const uint8_t* end) + : mCurrent(start), mEnd(end) { + ItemHeader* header = reinterpret_cast<ItemHeader*>(mCurrent); + if (header->type != T) { + advance(); + } + } + + filtered_iterator& operator++() { + advance(); + return *this; + } + + // Although this iterator self-terminates, we need a placeholder to compare against + // to make for-each loops happy + bool operator!=(const sentinal& other) const { + return mCurrent != mEnd; + } + + ItemContainer<T>& operator*() { + return *reinterpret_cast<ItemContainer<T>*>(mCurrent); + } + private: + void advance() { + ItemHeader* header = reinterpret_cast<ItemHeader*>(mCurrent); + do { + mCurrent += header->size; + header = reinterpret_cast<ItemHeader*>(mCurrent); + } while (mCurrent != mEnd && header->type != T); + } + uint8_t* mCurrent; + const uint8_t* const mEnd; + }; + + template <ItemTypes T> + class filtered_view { + public: + explicit filtered_view(uint8_t* start, const uint8_t* end) : mStart(start), mEnd(end) {} + + filtered_iterator<T> begin() const { + return filtered_iterator<T>{mStart, mEnd}; + } + + sentinal end() const { + return sentinal{mEnd}; + } + private: + uint8_t* mStart; + const uint8_t* const mEnd; + }; + + template <ItemTypes T> + filtered_view<T> filter() const { + return filtered_view<T>{start_ptr(), end_ptr()}; + } + +private: + + uint8_t* start_ptr() const { + return reinterpret_cast<uint8_t*>(mBuffer) + mBuffer->startOffset; + } + + const uint8_t* end_ptr() const { + return reinterpret_cast<uint8_t*>(mBuffer) + mBuffer->used; + } + + template <typename F, std::size_t... I> + void for_each(F&& f, std::index_sequence<I...>) const { + // Validate we're not empty + if (isEmpty()) return; + + // Setup the jump table, mapping from each type to a springboard that invokes the template + // function with the appropriate concrete type + using F_PTR = decltype(&f); + using THUNK = void (*)(F_PTR, void*); + static constexpr auto jump = std::array<THUNK, sizeof...(I)>{[](F_PTR fp, void* t) { + (*fp)(reinterpret_cast<const ItemContainer<static_cast<ItemTypes>(I)>*>(t)); + }...}; + + // Do the actual iteration of each item + uint8_t* current = start_ptr(); + const uint8_t* end = end_ptr(); + while (current != end) { + auto header = reinterpret_cast<ItemHeader*>(current); + // `f` could be a destructor, so ensure all accesses to the OP happen prior to invoking + // `f` + auto it = (void*)current; + current += header->size; + jump[static_cast<int>(header->type)](&f, it); + } + } + + void destroy() { + clear(); + resize(0); + } + + bool offsetIsValid(size_t offset) const { + return offset >= mBuffer->startOffset && offset < mBuffer->used; + } + + ItemHeader* itemAt(size_t offset) const { + if (!offsetIsValid(offset)) return nullptr; + return reinterpret_cast<ItemHeader*>(reinterpret_cast<uint8_t*>(mBuffer) + offset); + } + + bool isEmpty() const { return mBuffer == nullptr || mBuffer->used == STARTING_SIZE; } + + BufferHeader* mBuffer = nullptr; +}; + +template <typename ItemTypes, template <ItemTypes> typename ItemContainer, typename BufferHeader, + typename ItemTypeSequence> +void OpBuffer<ItemTypes, ItemContainer, BufferHeader, ItemTypeSequence>::clear() { + + // Don't need to do anything if we don't have a buffer + if (!mBuffer) return; + + for_each([](auto op) { + using T = std::remove_reference_t<decltype(*op)>; + op->~T(); + }); + mBuffer->used = STARTING_SIZE; + mBuffer->startOffset = STARTING_SIZE; + mBuffer->endOffset = 0; +} + +} // namespace android::uirenderer
\ No newline at end of file diff --git a/libs/hwui/canvas/Points.h b/libs/hwui/canvas/Points.h new file mode 100644 index 000000000000..05e6a7dd5884 --- /dev/null +++ b/libs/hwui/canvas/Points.h @@ -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. + */ +#include <ui/FatVector.h> +#include "SkPoint.h" +#include "SkRefCnt.h" + +/** + * Collection of points that are ref counted and to be used with + * various drawing calls that consume SkPoint as inputs like + * drawLines/drawPoints + */ +class Points: public SkNVRefCnt<SkPoint> { +public: + Points(int size){ + skPoints.resize(size); + } + + Points(std::initializer_list<SkPoint> init): skPoints(init) { } + + SkPoint& operator[](int index) { + return skPoints[index]; + } + + const SkPoint* data() const { + return skPoints.data(); + } + + size_t size() const { + return skPoints.size(); + } +private: + // Initialize the size to contain 2 SkPoints on the stack for optimized + // drawLine calls that require 2 SkPoints for start/end points of the line + android::FatVector<SkPoint, 2> skPoints; +}; diff --git a/libs/hwui/effects/StretchEffect.cpp b/libs/hwui/effects/StretchEffect.cpp new file mode 100644 index 000000000000..51cbc7592861 --- /dev/null +++ b/libs/hwui/effects/StretchEffect.cpp @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "StretchEffect.h" + +namespace android::uirenderer { + +sk_sp<SkImageFilter> StretchEffect::getImageFilter() const { + // TODO: Implement & Cache + // Probably need to use mutable to achieve caching + return nullptr; +} + +} // namespace android::uirenderer
\ No newline at end of file diff --git a/libs/hwui/effects/StretchEffect.h b/libs/hwui/effects/StretchEffect.h new file mode 100644 index 000000000000..7dfd6398765a --- /dev/null +++ b/libs/hwui/effects/StretchEffect.h @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "utils/MathUtils.h" + +#include <SkPoint.h> +#include <SkRect.h> +#include <SkImageFilter.h> + +namespace android::uirenderer { + +// TODO: Inherit from base RenderEffect type? +class StretchEffect { +public: + enum class StretchInterpolator { + SmoothStep, + }; + + bool isEmpty() const { + return MathUtils::isZero(stretchDirection.x()) + && MathUtils::isZero(stretchDirection.y()); + } + + void setEmpty() { + *this = StretchEffect{}; + } + + void mergeWith(const StretchEffect& other) { + if (other.isEmpty()) { + return; + } + if (isEmpty()) { + *this = other; + return; + } + stretchDirection += other.stretchDirection; + if (isEmpty()) { + return setEmpty(); + } + stretchArea.join(other.stretchArea); + maxStretchAmount = std::max(maxStretchAmount, other.maxStretchAmount); + } + + sk_sp<SkImageFilter> getImageFilter() const; + + SkRect stretchArea {0, 0, 0, 0}; + SkVector stretchDirection {0, 0}; + float maxStretchAmount = 0; +}; + +} // namespace android::uirenderer diff --git a/libs/hwui/hwui/AnimatedImageDrawable.cpp b/libs/hwui/hwui/AnimatedImageDrawable.cpp index 638de850a6c5..0d3d3e3f38fd 100644 --- a/libs/hwui/hwui/AnimatedImageDrawable.cpp +++ b/libs/hwui/hwui/AnimatedImageDrawable.cpp @@ -20,6 +20,7 @@ #endif #include "utils/TraceUtils.h" +#include "pipeline/skia/SkiaUtils.h" #include <SkPicture.h> #include <SkRefCnt.h> @@ -31,6 +32,7 @@ namespace android { AnimatedImageDrawable::AnimatedImageDrawable(sk_sp<SkAnimatedImage> animatedImage, size_t bytesUsed) : mSkAnimatedImage(std::move(animatedImage)), mBytesUsed(bytesUsed) { mTimeToShowNextSnapshot = ms2ns(mSkAnimatedImage->currentFrameDuration()); + setStagingBounds(mSkAnimatedImage->getBounds()); } void AnimatedImageDrawable::syncProperties() { @@ -127,21 +129,38 @@ AnimatedImageDrawable::Snapshot AnimatedImageDrawable::reset() { return snap; } +// Update the matrix to map from the intrinsic bounds of the SkAnimatedImage to +// the bounds specified by Drawable#setBounds. +static void handleBounds(SkMatrix* matrix, const SkRect& intrinsicBounds, const SkRect& bounds) { + matrix->preTranslate(bounds.left(), bounds.top()); + matrix->preScale(bounds.width() / intrinsicBounds.width(), + bounds.height() / intrinsicBounds.height()); +} + // Only called on the RenderThread. void AnimatedImageDrawable::onDraw(SkCanvas* canvas) { + // Store the matrix used to handle bounds and mirroring separate from the + // canvas. We may need to invert the matrix to determine the proper bounds + // to pass to saveLayer, and this matrix (as opposed to, potentially, the + // canvas' matrix) only uses scale and translate, so it must be invertible. + SkMatrix matrix; + SkAutoCanvasRestore acr(canvas, true); + handleBounds(&matrix, mSkAnimatedImage->getBounds(), mProperties.mBounds); + + if (mProperties.mMirrored) { + matrix.preTranslate(mSkAnimatedImage->getBounds().width(), 0); + matrix.preScale(-1, 1); + } + std::optional<SkPaint> lazyPaint; - SkAutoCanvasRestore acr(canvas, false); if (mProperties.mAlpha != SK_AlphaOPAQUE || mProperties.mColorFilter.get()) { lazyPaint.emplace(); lazyPaint->setAlpha(mProperties.mAlpha); lazyPaint->setColorFilter(mProperties.mColorFilter); lazyPaint->setFilterQuality(kLow_SkFilterQuality); } - if (mProperties.mMirrored) { - canvas->save(); - canvas->translate(mSkAnimatedImage->getBounds().width(), 0); - canvas->scale(-1, 1); - } + + canvas->concat(matrix); const bool starting = mStarting; mStarting = false; @@ -151,7 +170,11 @@ void AnimatedImageDrawable::onDraw(SkCanvas* canvas) { // The image is not animating, and never was. Draw directly from // mSkAnimatedImage. if (lazyPaint) { - canvas->saveLayer(mSkAnimatedImage->getBounds(), &*lazyPaint); + SkMatrix inverse; + (void) matrix.invert(&inverse); + SkRect r = mProperties.mBounds; + inverse.mapRect(&r); + canvas->saveLayer(r, &*lazyPaint); } std::unique_lock lock{mImageLock}; @@ -211,17 +234,31 @@ void AnimatedImageDrawable::onDraw(SkCanvas* canvas) { } int AnimatedImageDrawable::drawStaging(SkCanvas* canvas) { - SkAutoCanvasRestore acr(canvas, false); + // Store the matrix used to handle bounds and mirroring separate from the + // canvas. We may need to invert the matrix to determine the proper bounds + // to pass to saveLayer, and this matrix (as opposed to, potentially, the + // canvas' matrix) only uses scale and translate, so it must be invertible. + SkMatrix matrix; + SkAutoCanvasRestore acr(canvas, true); + handleBounds(&matrix, mSkAnimatedImage->getBounds(), mStagingProperties.mBounds); + + if (mStagingProperties.mMirrored) { + matrix.preTranslate(mSkAnimatedImage->getBounds().width(), 0); + matrix.preScale(-1, 1); + } + + canvas->concat(matrix); + if (mStagingProperties.mAlpha != SK_AlphaOPAQUE || mStagingProperties.mColorFilter.get()) { SkPaint paint; paint.setAlpha(mStagingProperties.mAlpha); paint.setColorFilter(mStagingProperties.mColorFilter); - canvas->saveLayer(mSkAnimatedImage->getBounds(), &paint); - } - if (mStagingProperties.mMirrored) { - canvas->save(); - canvas->translate(mSkAnimatedImage->getBounds().width(), 0); - canvas->scale(-1, 1); + + SkMatrix inverse; + (void) matrix.invert(&inverse); + SkRect r = mStagingProperties.mBounds; + inverse.mapRect(&r); + canvas->saveLayer(r, &paint); } if (!mRunning) { @@ -294,4 +331,10 @@ int AnimatedImageDrawable::drawStaging(SkCanvas* canvas) { return ns2ms(mTimeToShowNextSnapshot - mCurrentTime); } +SkRect AnimatedImageDrawable::onGetBounds() { + // This must return a bounds that is valid for all possible states, + // including if e.g. the client calls setBounds. + return SkRectMakeLargest(); +} + } // namespace android diff --git a/libs/hwui/hwui/AnimatedImageDrawable.h b/libs/hwui/hwui/AnimatedImageDrawable.h index f0aa35acf71b..8ca3c7e125f1 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. @@ -67,9 +67,10 @@ public: mStagingProperties.mColorFilter = filter; } void setStagingMirrored(bool mirrored) { mStagingProperties.mMirrored = mirrored; } + void setStagingBounds(const SkRect& bounds) { mStagingProperties.mBounds = bounds; } void syncProperties(); - virtual SkRect onGetBounds() override { return mSkAnimatedImage->getBounds(); } + SkRect onGetBounds() override; // Draw to software canvas, and return time to next draw. // 0 means the animation is not running. @@ -109,7 +110,7 @@ public: size_t byteSize() const { return sizeof(*this) + mBytesUsed; } protected: - virtual void onDraw(SkCanvas* canvas) override; + void onDraw(SkCanvas* canvas) override; private: sk_sp<SkAnimatedImage> mSkAnimatedImage; @@ -145,6 +146,7 @@ private: int mAlpha = SK_AlphaOPAQUE; sk_sp<SkColorFilter> mColorFilter; bool mMirrored = false; + SkRect mBounds; Properties() = default; Properties(Properties&) = default; 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..94a047c06ced 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. @@ -99,6 +100,12 @@ public: void getSkBitmap(SkBitmap* outBitmap); + SkBitmap getSkBitmap() { + SkBitmap ret; + getSkBitmap(&ret); + return ret; + } + int getAshmemFd() const; size_t getAllocationByteCount() const; @@ -160,11 +167,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 +183,6 @@ private: #endif virtual ~Bitmap(); - void* getStorage() const; SkImageInfo mInfo; @@ -191,10 +195,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..fdfa2883c33f 100644 --- a/libs/hwui/hwui/Canvas.h +++ b/libs/hwui/hwui/Canvas.h @@ -18,9 +18,9 @@ #include <cutils/compiler.h> #include <utils/Functor.h> +#include <SaveFlags.h> #include <androidfw/ResourceTypes.h> -#include "GlFunctorLifecycleListener.h" #include "Properties.h" #include "utils/Macros.h" @@ -30,9 +30,11 @@ class SkAnimatedImage; class SkCanvasState; +class SkRuntimeShaderBuilder; class SkVertices; namespace minikin { +class Font; class Layout; class MeasuredText; enum class Bidi : uint8_t; @@ -46,34 +48,6 @@ class CanvasPropertyPaint; class CanvasPropertyPrimitive; class DeferredLayerUpdater; class RenderNode; - -namespace skiapipeline { -class SkiaDisplayList; -} - -/** - * Data structure that holds the list of commands used in display list stream - */ -using DisplayList = skiapipeline::SkiaDisplayList; -} - -namespace SaveFlags { - -// These must match the corresponding Canvas API constants. -enum { - Matrix = 0x01, - Clip = 0x02, - HasAlphaLayer = 0x04, - ClipToLayer = 0x10, - - // Helper constant - MatrixClip = Matrix | Clip, -}; -typedef uint32_t Flags; - -} // namespace SaveFlags - -namespace uirenderer { namespace VectorDrawable { class Tree; } @@ -143,8 +117,8 @@ 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 finishRecording(uirenderer::RenderNode* destination) = 0; + virtual void enableZ(bool enableZ) = 0; bool isHighContrastText() const { return uirenderer::Properties::enableHighContrastText; } @@ -159,11 +133,16 @@ public: uirenderer::CanvasPropertyPrimitive* y, uirenderer::CanvasPropertyPrimitive* radius, uirenderer::CanvasPropertyPaint* paint) = 0; + virtual void drawRipple(uirenderer::CanvasPropertyPrimitive* x, + uirenderer::CanvasPropertyPrimitive* y, + uirenderer::CanvasPropertyPrimitive* radius, + uirenderer::CanvasPropertyPaint* paint, + uirenderer::CanvasPropertyPrimitive* progress, + const SkRuntimeShaderBuilder& effectBuilder) = 0; 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"); } @@ -179,10 +158,8 @@ public: virtual void restoreToCount(int saveCount) = 0; virtual void restoreUnclippedLayer(int saveCount, const SkPaint& paint) = 0; - virtual int saveLayer(float left, float top, float right, float bottom, const SkPaint* paint, - SaveFlags::Flags flags) = 0; - virtual int saveLayerAlpha(float left, float top, float right, float bottom, int alpha, - SaveFlags::Flags flags) = 0; + virtual int saveLayer(float left, float top, float right, float bottom, const SkPaint* paint) = 0; + virtual int saveLayerAlpha(float left, float top, float right, float bottom, int alpha) = 0; virtual int saveUnclippedLayer(int, int, int, int) = 0; // Matrix @@ -257,6 +234,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 +270,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/ImageDecoder.cpp b/libs/hwui/hwui/ImageDecoder.cpp index 43cc4f244f71..ade63e5b832c 100644 --- a/libs/hwui/hwui/ImageDecoder.cpp +++ b/libs/hwui/hwui/ImageDecoder.cpp @@ -17,11 +17,19 @@ #include "ImageDecoder.h" #include <hwui/Bitmap.h> +#include <log/log.h> #include <SkAndroidCodec.h> +#include <SkBitmap.h> +#include <SkBlendMode.h> #include <SkCanvas.h> +#include <SkEncodedOrigin.h> +#include <SkFilterQuality.h> #include <SkPaint.h> +#undef LOG_TAG +#define LOG_TAG "ImageDecoder" + using namespace android; sk_sp<SkColorSpace> ImageDecoder::getDefaultColorSpace() const { @@ -40,20 +48,39 @@ sk_sp<SkColorSpace> ImageDecoder::getDefaultColorSpace() const { ImageDecoder::ImageDecoder(std::unique_ptr<SkAndroidCodec> codec, sk_sp<SkPngChunkReader> peeker) : mCodec(std::move(codec)) , mPeeker(std::move(peeker)) - , mTargetSize(mCodec->getInfo().dimensions()) - , mDecodeSize(mTargetSize) + , mDecodeSize(mCodec->codec()->dimensions()) , mOutColorType(mCodec->computeOutputColorType(kN32_SkColorType)) , mUnpremultipliedRequired(false) , mOutColorSpace(getDefaultColorSpace()) - , mSampleSize(1) + , mHandleRestorePrevious(true) { + mTargetSize = swapWidthHeight() ? SkISize { mDecodeSize.height(), mDecodeSize.width() } + : mDecodeSize; + this->rewind(); } +ImageDecoder::~ImageDecoder() = default; + SkAlphaType ImageDecoder::getOutAlphaType() const { return opaque() ? kOpaque_SkAlphaType : mUnpremultipliedRequired ? kUnpremul_SkAlphaType : kPremul_SkAlphaType; } +static SkISize swapped(const SkISize& size) { + return SkISize { size.height(), size.width() }; +} + +static bool requires_matrix_scaling(bool swapWidthHeight, const SkISize& decodeSize, + const SkISize& targetSize) { + return (swapWidthHeight && decodeSize != swapped(targetSize)) + || (!swapWidthHeight && decodeSize != targetSize); +} + +SkISize ImageDecoder::getSampledDimensions(int sampleSize) const { + auto size = mCodec->getSampledDimensions(sampleSize); + return swapWidthHeight() ? swapped(size) : size; +} + bool ImageDecoder::setTargetSize(int width, int height) { if (width <= 0 || height <= 0) { return false; @@ -77,16 +104,21 @@ bool ImageDecoder::setTargetSize(int width, int height) { } } - SkISize targetSize = { width, height }, decodeSize = targetSize; + const bool swap = swapWidthHeight(); + const SkISize targetSize = { width, height }; + SkISize decodeSize = swap ? SkISize { height, width } : targetSize; int sampleSize = mCodec->computeSampleSize(&decodeSize); - if (decodeSize != targetSize && mUnpremultipliedRequired && !opaque()) { - return false; + if (mUnpremultipliedRequired && !opaque()) { + // Allow using a matrix to handle orientation, but not scaling. + if (requires_matrix_scaling(swap, decodeSize, targetSize)) { + return false; + } } mTargetSize = targetSize; mDecodeSize = decodeSize; - mSampleSize = sampleSize; + mOptions.fSampleSize = sampleSize; return true; } @@ -135,8 +167,10 @@ bool ImageDecoder::setOutColorType(SkColorType colorType) { } bool ImageDecoder::setUnpremultipliedRequired(bool required) { - if (required && !opaque() && mDecodeSize != mTargetSize) { - return false; + if (required && !opaque()) { + if (requires_matrix_scaling(swapWidthHeight(), mDecodeSize, mTargetSize)) { + return false; + } } mUnpremultipliedRequired = required; return true; @@ -157,25 +191,239 @@ SkImageInfo ImageDecoder::getOutputInfo() const { return SkImageInfo::Make(size, mOutColorType, getOutAlphaType(), getOutputColorSpace()); } +bool ImageDecoder::swapWidthHeight() const { + return SkEncodedOriginSwapsWidthHeight(mCodec->codec()->getOrigin()); +} + +int ImageDecoder::width() const { + return swapWidthHeight() + ? mCodec->codec()->dimensions().height() + : mCodec->codec()->dimensions().width(); +} + +int ImageDecoder::height() const { + return swapWidthHeight() + ? mCodec->codec()->dimensions().width() + : mCodec->codec()->dimensions().height(); +} + bool ImageDecoder::opaque() const { - return mCodec->getInfo().alphaType() == kOpaque_SkAlphaType; + return mCurrentFrameIsOpaque; } bool ImageDecoder::gray() const { return mCodec->getInfo().colorType() == kGray_8_SkColorType; } +bool ImageDecoder::isAnimated() { + return mCodec->codec()->getFrameCount() > 1; +} + +int ImageDecoder::currentFrame() const { + return mOptions.fFrameIndex; +} + +bool ImageDecoder::rewind() { + mOptions.fFrameIndex = 0; + mOptions.fPriorFrame = SkCodec::kNoFrame; + mCurrentFrameIsIndependent = true; + mCurrentFrameIsOpaque = mCodec->getInfo().isOpaque(); + mRestoreState = RestoreState::kDoNothing; + mRestoreFrame = nullptr; + + // TODO: Rewind the input now instead of in the next call to decode, and + // plumb through whether rewind succeeded. + return true; +} + +void ImageDecoder::setHandleRestorePrevious(bool handle) { + mHandleRestorePrevious = handle; + if (!handle) { + mRestoreFrame = nullptr; + } +} + +bool ImageDecoder::advanceFrame() { + const int frameIndex = ++mOptions.fFrameIndex; + const int frameCount = mCodec->codec()->getFrameCount(); + if (frameIndex >= frameCount) { + // Prevent overflow from repeated calls to advanceFrame. + mOptions.fFrameIndex = frameCount; + return false; + } + + SkCodec::FrameInfo frameInfo; + if (!mCodec->codec()->getFrameInfo(frameIndex, &frameInfo) + || !frameInfo.fFullyReceived) { + // Mark the decoder as finished, requiring a rewind. + mOptions.fFrameIndex = frameCount; + return false; + } + + mCurrentFrameIsIndependent = frameInfo.fRequiredFrame == SkCodec::kNoFrame; + mCurrentFrameIsOpaque = frameInfo.fAlphaType == kOpaque_SkAlphaType; + + if (frameInfo.fDisposalMethod == SkCodecAnimation::DisposalMethod::kRestorePrevious) { + switch (mRestoreState) { + case RestoreState::kDoNothing: + case RestoreState::kNeedsRestore: + mRestoreState = RestoreState::kFirstRPFrame; + mOptions.fPriorFrame = frameIndex - 1; + break; + case RestoreState::kFirstRPFrame: + mRestoreState = RestoreState::kRPFrame; + break; + case RestoreState::kRPFrame: + // Unchanged. + break; + } + } else { // New frame is not restore previous + switch (mRestoreState) { + case RestoreState::kFirstRPFrame: + case RestoreState::kRPFrame: + mRestoreState = RestoreState::kNeedsRestore; + break; + case RestoreState::kNeedsRestore: + mRestoreState = RestoreState::kDoNothing; + mRestoreFrame = nullptr; + [[fallthrough]]; + case RestoreState::kDoNothing: + mOptions.fPriorFrame = frameIndex - 1; + break; + } + } + + return true; +} + +SkCodec::FrameInfo ImageDecoder::getCurrentFrameInfo() { + LOG_ALWAYS_FATAL_IF(finished()); + + auto dims = mCodec->codec()->dimensions(); + SkCodec::FrameInfo info; + if (!mCodec->codec()->getFrameInfo(mOptions.fFrameIndex, &info)) { + // SkCodec may return false for a non-animated image. Provide defaults. + info.fRequiredFrame = SkCodec::kNoFrame; + info.fDuration = 0; + info.fFullyReceived = true; + info.fAlphaType = mCodec->codec()->getInfo().alphaType(); + info.fHasAlphaWithinBounds = info.fAlphaType != kOpaque_SkAlphaType; + info.fDisposalMethod = SkCodecAnimation::DisposalMethod::kKeep; + info.fBlend = SkCodecAnimation::Blend::kSrc; + info.fFrameRect = SkIRect::MakeSize(dims); + } + + if (auto origin = mCodec->codec()->getOrigin(); origin != kDefault_SkEncodedOrigin) { + if (SkEncodedOriginSwapsWidthHeight(origin)) { + dims = swapped(dims); + } + auto matrix = SkEncodedOriginToMatrix(origin, dims.width(), dims.height()); + auto rect = SkRect::Make(info.fFrameRect); + LOG_ALWAYS_FATAL_IF(!matrix.mapRect(&rect)); + rect.roundIn(&info.fFrameRect); + } + return info; +} + +bool ImageDecoder::finished() const { + return mOptions.fFrameIndex >= mCodec->codec()->getFrameCount(); +} + +bool ImageDecoder::handleRestorePrevious(const SkImageInfo& outputInfo, void* pixels, + size_t rowBytes) { + if (!mHandleRestorePrevious) { + return true; + } + + switch (mRestoreState) { + case RestoreState::kFirstRPFrame:{ + // This frame is marked kRestorePrevious. The prior frame should be in + // |pixels|, and it is what we'll restore after each consecutive + // kRestorePrevious frame. Cache it now. + if (!(mRestoreFrame = Bitmap::allocateHeapBitmap(outputInfo))) { + return false; + } + + const uint8_t* srcRow = static_cast<uint8_t*>(pixels); + uint8_t* dstRow = static_cast<uint8_t*>(mRestoreFrame->pixels()); + for (int y = 0; y < outputInfo.height(); y++) { + memcpy(dstRow, srcRow, outputInfo.minRowBytes()); + srcRow += rowBytes; + dstRow += mRestoreFrame->rowBytes(); + } + break; + } + case RestoreState::kRPFrame: + case RestoreState::kNeedsRestore: + // Restore the cached frame. It's possible that the client skipped decoding a frame, so + // we never cached it. + if (mRestoreFrame) { + const uint8_t* srcRow = static_cast<uint8_t*>(mRestoreFrame->pixels()); + uint8_t* dstRow = static_cast<uint8_t*>(pixels); + for (int y = 0; y < outputInfo.height(); y++) { + memcpy(dstRow, srcRow, outputInfo.minRowBytes()); + srcRow += mRestoreFrame->rowBytes(); + dstRow += rowBytes; + } + } + break; + case RestoreState::kDoNothing: + break; + } + return true; +} + SkCodec::Result ImageDecoder::decode(void* pixels, size_t rowBytes) { + // This was checked inside setTargetSize, but it's possible the first frame + // was opaque, so that method succeeded, but after calling advanceFrame, the + // current frame is not opaque. + if (mUnpremultipliedRequired && !opaque()) { + // Allow using a matrix to handle orientation, but not scaling. + if (requires_matrix_scaling(swapWidthHeight(), mDecodeSize, mTargetSize)) { + return SkCodec::kInvalidScale; + } + } + + const auto outputInfo = getOutputInfo(); + if (!handleRestorePrevious(outputInfo, pixels, rowBytes)) { + return SkCodec::kInternalError; + } + void* decodePixels = pixels; size_t decodeRowBytes = rowBytes; - auto decodeInfo = SkImageInfo::Make(mDecodeSize, mOutColorType, getOutAlphaType(), - getOutputColorSpace()); + const auto decodeInfo = SkImageInfo::Make(mDecodeSize, mOutColorType, getOutAlphaType(), + getOutputColorSpace()); // Used if we need a temporary before scaling or subsetting. // FIXME: Use scanline decoding on only a couple lines to save memory. b/70709380. SkBitmap tmp; const bool scale = mDecodeSize != mTargetSize; - if (scale || mCropRect) { - if (!tmp.setInfo(decodeInfo)) { + const auto origin = mCodec->codec()->getOrigin(); + const bool handleOrigin = origin != kDefault_SkEncodedOrigin; + SkMatrix outputMatrix; + if (scale || handleOrigin || mCropRect) { + if (mCropRect) { + outputMatrix.setTranslate(-mCropRect->fLeft, -mCropRect->fTop); + } + + int targetWidth = mTargetSize.width(); + int targetHeight = mTargetSize.height(); + if (handleOrigin) { + outputMatrix.preConcat(SkEncodedOriginToMatrix(origin, targetWidth, targetHeight)); + if (SkEncodedOriginSwapsWidthHeight(origin)) { + std::swap(targetWidth, targetHeight); + } + } + if (scale) { + float scaleX = (float) targetWidth / mDecodeSize.width(); + float scaleY = (float) targetHeight / mDecodeSize.height(); + outputMatrix.preScale(scaleX, scaleY); + } + // It's possible that this portion *does* have alpha, even if the + // composed frame does not. In that case, the SkBitmap needs to have + // alpha so it blends properly. + if (!tmp.setInfo(decodeInfo.makeAlphaType(mUnpremultipliedRequired ? kUnpremul_SkAlphaType + : kPremul_SkAlphaType))) + { return SkCodec::kInternalError; } if (!Bitmap::allocateHeapBitmap(&tmp)) { @@ -183,33 +431,38 @@ SkCodec::Result ImageDecoder::decode(void* pixels, size_t rowBytes) { } decodePixels = tmp.getPixels(); decodeRowBytes = tmp.rowBytes(); + + if (!mCurrentFrameIsIndependent) { + SkMatrix inverse; + if (outputMatrix.invert(&inverse)) { + SkCanvas canvas(tmp, SkCanvas::ColorBehavior::kLegacy); + canvas.setMatrix(inverse); + SkBitmap priorFrame; + priorFrame.installPixels(outputInfo, pixels, rowBytes); + priorFrame.setImmutable(); // Don't want asImage() to force a copy + canvas.drawImage(priorFrame.asImage(), 0, 0, + SkSamplingOptions(SkFilterMode::kLinear)); + } else { + ALOGE("Failed to invert matrix!"); + } + } } - SkAndroidCodec::AndroidOptions options; - options.fSampleSize = mSampleSize; - auto result = mCodec->getAndroidPixels(decodeInfo, decodePixels, decodeRowBytes, &options); + auto result = mCodec->getAndroidPixels(decodeInfo, decodePixels, decodeRowBytes, &mOptions); - if (scale || mCropRect) { + if (scale || handleOrigin || mCropRect) { SkBitmap scaledBm; - if (!scaledBm.installPixels(getOutputInfo(), pixels, rowBytes)) { + if (!scaledBm.installPixels(outputInfo, pixels, rowBytes)) { return SkCodec::kInternalError; } SkPaint paint; paint.setBlendMode(SkBlendMode::kSrc); - paint.setFilterQuality(kLow_SkFilterQuality); // bilinear filtering SkCanvas canvas(scaledBm, SkCanvas::ColorBehavior::kLegacy); - if (mCropRect) { - canvas.translate(-mCropRect->fLeft, -mCropRect->fTop); - } - if (scale) { - float scaleX = (float) mTargetSize.width() / mDecodeSize.width(); - float scaleY = (float) mTargetSize.height() / mDecodeSize.height(); - canvas.scale(scaleX, scaleY); - } - - canvas.drawBitmap(tmp, 0.0f, 0.0f, &paint); + canvas.setMatrix(outputMatrix); + tmp.setImmutable(); // Don't want asImage() to force copy + canvas.drawImage(tmp.asImage(), 0, 0, SkSamplingOptions(SkFilterMode::kLinear), &paint); } return result; diff --git a/libs/hwui/hwui/ImageDecoder.h b/libs/hwui/hwui/ImageDecoder.h index a1b51573db3f..cbfffd5e9291 100644 --- a/libs/hwui/hwui/ImageDecoder.h +++ b/libs/hwui/hwui/ImageDecoder.h @@ -15,6 +15,7 @@ */ #pragma once +#include <SkAndroidCodec.h> #include <SkCodec.h> #include <SkImageInfo.h> #include <SkPngChunkReader.h> @@ -24,18 +25,20 @@ #include <optional> -class SkAndroidCodec; - namespace android { -class ANDROID_API ImageDecoder { +class Bitmap; + +class ANDROID_API ImageDecoder final { public: std::unique_ptr<SkAndroidCodec> mCodec; sk_sp<SkPngChunkReader> mPeeker; ImageDecoder(std::unique_ptr<SkAndroidCodec> codec, sk_sp<SkPngChunkReader> peeker = nullptr); + ~ImageDecoder(); + SkISize getSampledDimensions(int sampleSize) const; bool setTargetSize(int width, int height); bool setCropRect(const SkIRect*); @@ -46,21 +49,69 @@ public: sk_sp<SkColorSpace> getDefaultColorSpace() const; void setOutColorSpace(sk_sp<SkColorSpace> cs); - // The size is the final size after scaling and cropping. + // The size is the final size after scaling, adjusting for the origin, and + // cropping. SkImageInfo getOutputInfo() const; + int width() const; + int height() const; + + // True if the current frame is opaque. bool opaque() const; + bool gray() const; SkCodec::Result decode(void* pixels, size_t rowBytes); + // Return true if the decoder has advanced beyond all frames. + bool finished() const; + + bool advanceFrame(); + bool rewind(); + + bool isAnimated(); + int currentFrame() const; + + SkCodec::FrameInfo getCurrentFrameInfo(); + + // Set whether the ImageDecoder should handle RestorePrevious frames. + void setHandleRestorePrevious(bool handle); + private: + // State machine for keeping track of how to handle RestorePrevious (RP) + // frames in decode(). + enum class RestoreState { + // Neither this frame nor the prior is RP, so there is no need to cache + // or restore. + kDoNothing, + + // This is the first in a sequence of one or more RP frames. decode() + // needs to cache the provided pixels. + kFirstRPFrame, + + // This is the second (or later) in a sequence of multiple RP frames. + // decode() needs to restore the cached frame that preceded the first RP + // frame in the sequence. + kRPFrame, + + // This is the first non-RP frame after a sequence of one or more RP + // frames. decode() still needs to restore the cached frame. Separate + // from kRPFrame because if the following frame is RP the state will + // change to kFirstRPFrame. + kNeedsRestore, + }; + SkISize mTargetSize; SkISize mDecodeSize; SkColorType mOutColorType; bool mUnpremultipliedRequired; sk_sp<SkColorSpace> mOutColorSpace; - int mSampleSize; + SkAndroidCodec::AndroidOptions mOptions; + bool mCurrentFrameIsIndependent; + bool mCurrentFrameIsOpaque; + bool mHandleRestorePrevious; + RestoreState mRestoreState; + sk_sp<Bitmap> mRestoreFrame; std::optional<SkIRect> mCropRect; ImageDecoder(const ImageDecoder&) = delete; @@ -68,6 +119,9 @@ private: SkAlphaType getOutAlphaType() const; sk_sp<SkColorSpace> getOutputColorSpace() const; + bool swapWidthHeight() const; + // Store/restore a frame if necessary. Returns false on error. + bool handleRestorePrevious(const SkImageInfo&, void* pixels, size_t rowBytes); }; } // namespace android 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..05bae5c9f778 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. @@ -137,6 +137,9 @@ public: bool isDevKern() const { return mDevKern; } void setDevKern(bool d) { mDevKern = d; } + // Deprecated -- bitmapshaders will be taking this flag explicitly + bool isFilterBitmap() const { return this->getFilterQuality() != kNone_SkFilterQuality; } + // The Java flags (Paint.java) no longer fit into the native apis directly. // These methods handle converting to and from them and the native representations // in android::Paint. @@ -149,7 +152,7 @@ public: // The only respected flags are : [ antialias, dither, filterBitmap ] static uint32_t GetSkPaintJavaFlags(const SkPaint&); static void SetSkPaintJavaFlags(SkPaint*, uint32_t flags); - + private: SkFont mFont; sk_sp<SkDrawLooper> mLooper; 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/hwui/Typeface.h b/libs/hwui/hwui/Typeface.h index ef8d8f4ee4f3..0c3ef01ab26b 100644 --- a/libs/hwui/hwui/Typeface.h +++ b/libs/hwui/hwui/Typeface.h @@ -41,6 +41,9 @@ public: enum Style : uint8_t { kNormal = 0, kBold = 0x01, kItalic = 0x02, kBoldItalic = 0x03 }; Style fAPIStyle; + // base weight in CSS-style units, 1..1000 + int fBaseWeight; + static const Typeface* resolveDefault(const Typeface* src); // The following three functions create new Typeface from an existing Typeface with a different @@ -81,10 +84,6 @@ public: // Sets roboto font as the default typeface for testing purpose. static void setRobotoTypefaceForTest(); - -private: - // base weight in CSS-style units, 1..1000 - int fBaseWeight; }; } diff --git a/libs/hwui/jni/AnimatedImageDrawable.cpp b/libs/hwui/jni/AnimatedImageDrawable.cpp index 1ff156593c41..c9433ec8a9da 100644 --- a/libs/hwui/jni/AnimatedImageDrawable.cpp +++ b/libs/hwui/jni/AnimatedImageDrawable.cpp @@ -244,6 +244,14 @@ static void AnimatedImageDrawable_nSetMirrored(JNIEnv* env, jobject /*clazz*/, j drawable->setStagingMirrored(mirrored); } +static void AnimatedImageDrawable_nSetBounds(JNIEnv* env, jobject /*clazz*/, jlong nativePtr, + jobject jrect) { + auto* drawable = reinterpret_cast<AnimatedImageDrawable*>(nativePtr); + SkRect rect; + GraphicsJNI::jrect_to_rect(env, jrect, &rect); + drawable->setStagingBounds(rect); +} + static const JNINativeMethod gAnimatedImageDrawableMethods[] = { { "nCreate", "(JLandroid/graphics/ImageDecoder;IIJZLandroid/graphics/Rect;)J",(void*) AnimatedImageDrawable_nCreate }, { "nGetNativeFinalizer", "()J", (void*) AnimatedImageDrawable_nGetNativeFinalizer }, @@ -259,6 +267,7 @@ static const JNINativeMethod gAnimatedImageDrawableMethods[] = { { "nSetOnAnimationEndListener", "(JLandroid/graphics/drawable/AnimatedImageDrawable;)V", (void*) AnimatedImageDrawable_nSetOnAnimationEndListener }, { "nNativeByteSize", "(J)J", (void*) AnimatedImageDrawable_nNativeByteSize }, { "nSetMirrored", "(JZ)V", (void*) AnimatedImageDrawable_nSetMirrored }, + { "nSetBounds", "(JLandroid/graphics/Rect;)V", (void*) AnimatedImageDrawable_nSetBounds }, }; int register_android_graphics_drawable_AnimatedImageDrawable(JNIEnv* env) { diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp index c0663a9bc699..05278f24ebbd 100755 --- a/libs/hwui/jni/Bitmap.cpp +++ b/libs/hwui/jni/Bitmap.cpp @@ -3,11 +3,12 @@ #include "Bitmap.h" #include "SkBitmap.h" +#include "SkCanvas.h" +#include "SkColor.h" +#include "SkColorSpace.h" #include "SkPixelRef.h" #include "SkImageEncoder.h" #include "SkImageInfo.h" -#include "SkColor.h" -#include "SkColorSpace.h" #include "GraphicsJNI.h" #include "SkStream.h" #include "SkWebpEncoder.h" @@ -19,12 +20,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 +575,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 +877,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 +918,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 +1205,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 +1225,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 +1299,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 +1321,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..4e9daa4b0c16 100644 --- a/libs/hwui/jni/BitmapFactory.cpp +++ b/libs/hwui/jni/BitmapFactory.cpp @@ -3,15 +3,16 @@ #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 "SkCanvas.h" #include "SkMath.h" #include "SkPixelRef.h" #include "SkStream.h" +#include "SkString.h" #include "SkUtils.h" #include "Utils.h" @@ -68,6 +69,8 @@ const char* getMimeType(SkEncodedImageFormat format) { return "image/webp"; case SkEncodedImageFormat::kHEIF: return "image/heif"; + case SkEncodedImageFormat::kAVIF: + return "image/avif"; case SkEncodedImageFormat::kWBMP: return "image/vnd.wap.wbmp"; case SkEncodedImageFormat::kDNG: @@ -454,11 +457,12 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream, // outputBitmap. Otherwise we would blend by default, which is not // what we want. paint.setBlendMode(SkBlendMode::kSrc); - paint.setFilterQuality(kLow_SkFilterQuality); // bilinear filtering SkCanvas canvas(outputBitmap, SkCanvas::ColorBehavior::kLegacy); canvas.scale(scaleX, scaleY); - canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint); + decodingBitmap.setImmutable(); // so .asImage() doesn't make a copy + canvas.drawImage(decodingBitmap.asImage(), 0.0f, 0.0f, + SkSamplingOptions(SkFilterMode::kLinear), &paint); } else { outputBitmap.swap(decodingBitmap); } @@ -510,8 +514,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 +569,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..ba407f2164de 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,14 @@ #include "graphics_jni_helpers.h" -class SkBitmapRegionDecoder; class SkCanvas; +struct SkFontMetrics; namespace android { +namespace skia { + class BitmapRegionDecoder; +} +class Canvas; class Paint; struct Typeface; } @@ -83,6 +87,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 +118,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 +170,7 @@ private: static JavaVM* mJavaVM; }; -class HeapAllocator : public SkBRDAllocator { +class HeapAllocator : public android::skia::BRDAllocator { public: HeapAllocator() { }; ~HeapAllocator() { }; @@ -181,7 +197,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 +216,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..ad7741b61e9f 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> @@ -135,19 +135,15 @@ static jobject native_create(JNIEnv* env, std::unique_ptr<SkStream> stream, return throw_exception(env, kSourceException, "", jexception, source); } - auto androidCodec = SkAndroidCodec::MakeFromCodec(std::move(codec), - SkAndroidCodec::ExifOrientationBehavior::kRespect); + auto androidCodec = SkAndroidCodec::MakeFromCodec(std::move(codec)); if (!androidCodec.get()) { return throw_exception(env, kSourceMalformedData, "", nullptr, source); } - const auto& info = androidCodec->getInfo(); - const int width = info.width(); - const int height = info.height(); const bool isNinePatch = peeker->mPatch != nullptr; ImageDecoder* decoder = new ImageDecoder(std::move(androidCodec), std::move(peeker)); return env->NewObject(gImageDecoder_class, gImageDecoder_constructorMethodID, - reinterpret_cast<jlong>(decoder), width, height, + reinterpret_cast<jlong>(decoder), decoder->width(), decoder->height(), animated, isNinePatch); } @@ -194,8 +190,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); } @@ -470,7 +465,7 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong static jobject ImageDecoder_nGetSampledSize(JNIEnv* env, jobject /*clazz*/, jlong nativePtr, jint sampleSize) { auto* decoder = reinterpret_cast<ImageDecoder*>(nativePtr); - SkISize size = decoder->mCodec->getSampledDimensions(sampleSize); + SkISize size = decoder->getSampledDimensions(sampleSize); return env->NewObject(gSize_class, gSize_constructorMethodID, size.width(), size.height()); } 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..97c40d695f97 --- /dev/null +++ b/libs/hwui/jni/RenderEffect.cpp @@ -0,0 +1,140 @@ +/* + * 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 jlong createBitmapEffect( + JNIEnv* env, + jobject, + jlong bitmapHandle, + jfloat srcLeft, + jfloat srcTop, + jfloat srcRight, + jfloat srcBottom, + jfloat dstLeft, + jfloat dstTop, + jfloat dstRight, + jfloat dstBottom +) { + sk_sp<SkImage> image = android::bitmap::toBitmap(bitmapHandle).makeImage(); + SkRect srcRect = SkRect::MakeLTRB(srcLeft, srcTop, srcRight, srcBottom); + SkRect dstRect = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom); + sk_sp<SkImageFilter> bitmapFilter = + SkImageFilters::Image(image, srcRect, dstRect, kLow_SkFilterQuality); + return reinterpret_cast<jlong>(bitmapFilter.release()); +} + +static jlong createColorFilterEffect( + JNIEnv* env, + jobject, + jlong colorFilterHandle, + jlong inputFilterHandle +) { + auto* colorFilter = reinterpret_cast<const SkColorFilter*>(colorFilterHandle); + auto* inputFilter = reinterpret_cast<const SkImageFilter*>(inputFilterHandle); + sk_sp<SkImageFilter> colorFilterImageFilter = SkImageFilters::ColorFilter( + sk_ref_sp(colorFilter), sk_ref_sp(inputFilter), nullptr); + return reinterpret_cast<jlong>(colorFilterImageFilter.release()); +} + +static jlong createBlendModeEffect( + JNIEnv* env, + jobject, + jlong backgroundImageFilterHandle, + jlong foregroundImageFilterHandle, + jint blendmodeHandle +) { + auto* backgroundFilter = reinterpret_cast<const SkImageFilter*>(backgroundImageFilterHandle); + auto* foregroundFilter = reinterpret_cast<const SkImageFilter*>(foregroundImageFilterHandle); + SkBlendMode blendMode = static_cast<SkBlendMode>(blendmodeHandle); + sk_sp<SkImageFilter> xfermodeFilter = SkImageFilters::Blend( + blendMode, + sk_ref_sp(backgroundFilter), + sk_ref_sp(foregroundFilter) + ); + return reinterpret_cast<jlong>(xfermodeFilter.release()); +} + +static jlong createChainEffect( + JNIEnv* env, + jobject, + jlong outerFilterHandle, + jlong innerFilterHandle +) { + auto* outerImageFilter = reinterpret_cast<const SkImageFilter*>(outerFilterHandle); + auto* innerImageFilter = reinterpret_cast<const SkImageFilter*>(innerFilterHandle); + sk_sp<SkImageFilter> composeFilter = SkImageFilters::Compose( + sk_ref_sp(outerImageFilter), + sk_ref_sp(innerImageFilter) + ); + return reinterpret_cast<jlong>(composeFilter.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}, + {"nativeCreateBitmapEffect", "(JFFFFFFFF)J", (void*)createBitmapEffect}, + {"nativeCreateColorFilterEffect", "(JJ)J", (void*)createColorFilterEffect}, + {"nativeCreateBlendModeEffect", "(JJI)J", (void*)createBlendModeEffect}, + {"nativeCreateChainEffect", "(JJ)J", (void*)createChainEffect} +}; + +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..1dc5cd99eed1 100644 --- a/libs/hwui/jni/Shader.cpp +++ b/libs/hwui/jni/Shader.cpp @@ -1,3 +1,6 @@ +#undef LOG_TAG +#define LOG_TAG "ShaderJNI" + #include "GraphicsJNI.h" #include "SkColorFilter.h" #include "SkGradientShader.h" @@ -61,7 +64,7 @@ static jlong Shader_getNativeFinalizer(JNIEnv*, jobject) { /////////////////////////////////////////////////////////////////////////////////////////////// static jlong BitmapShader_constructor(JNIEnv* env, jobject o, jlong matrixPtr, jlong bitmapHandle, - jint tileModeX, jint tileModeY) { + jint tileModeX, jint tileModeY, bool filter) { const SkMatrix* matrix = reinterpret_cast<const SkMatrix*>(matrixPtr); sk_sp<SkImage> image; if (bitmapHandle) { @@ -74,8 +77,10 @@ static jlong BitmapShader_constructor(JNIEnv* env, jobject o, jlong matrixPtr, j SkBitmap bitmap; image = SkMakeImageFromRasterBitmap(bitmap, kNever_SkCopyPixelsMode); } + SkSamplingOptions sampling(filter ? SkFilterMode::kLinear : SkFilterMode::kNearest, + SkMipmapMode::kNone); sk_sp<SkShader> shader = image->makeShader( - (SkTileMode)tileModeX, (SkTileMode)tileModeY); + (SkTileMode)tileModeX, (SkTileMode)tileModeY, sampling); ThrowIAE_IfNull(env, shader.get()); if (matrix) { @@ -133,11 +138,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 +167,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); @@ -210,38 +235,73 @@ 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) { - SkRuntimeEffect* effect = reinterpret_cast<SkRuntimeEffect*>(shaderFactory); - AutoJavaByteArray arInputs(env, inputs); +/////////////////////////////////////////////////////////////////////////////////////////////// + +static jlong RuntimeShader_createShaderBuilder(JNIEnv* env, jobject, jstring sksl) { + ScopedUtfChars strSksl(env, sksl); + auto result = SkRuntimeEffect::Make(SkString(strSksl.c_str())); + sk_sp<SkRuntimeEffect> effect = std::get<0>(result); + if (effect.get() == nullptr) { + const auto& err = std::get<1>(result); + doThrowIAE(env, err.c_str()); + return 0; + } + return reinterpret_cast<jlong>(new SkRuntimeShaderBuilder(std::move(effect))); +} + +static void SkRuntimeShaderBuilder_delete(SkRuntimeShaderBuilder* builder) { + delete builder; +} + +static jlong RuntimeShader_getNativeFinalizer(JNIEnv*, jobject) { + return static_cast<jlong>(reinterpret_cast<uintptr_t>(&SkRuntimeShaderBuilder_delete)); +} - sk_sp<SkData> fData; - fData = SkData::MakeWithCopy(arInputs.ptr(), arInputs.length()); +static jlong RuntimeShader_create(JNIEnv* env, jobject, jlong shaderBuilder, jlong matrixPtr, + jboolean isOpaque) { + SkRuntimeShaderBuilder* builder = reinterpret_cast<SkRuntimeShaderBuilder*>(shaderBuilder); 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 = builder->makeShader(matrix, isOpaque == JNI_TRUE); ThrowIAE_IfNull(env, shader); - return reinterpret_cast<jlong>(shader.release()); } -/////////////////////////////////////////////////////////////////////////////////////////////// - -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); +static inline int ThrowIAEFmt(JNIEnv* env, const char* fmt, ...) { + va_list args; + va_start(args, fmt); + int ret = jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", fmt, args); + va_end(args); + return ret; +} - return reinterpret_cast<jlong>(effect.release()); +static void RuntimeShader_updateUniforms(JNIEnv* env, jobject, jlong shaderBuilder, + jstring jUniformName, jfloatArray jvalues) { + SkRuntimeShaderBuilder* builder = reinterpret_cast<SkRuntimeShaderBuilder*>(shaderBuilder); + ScopedUtfChars name(env, jUniformName); + AutoJavaFloatArray autoValues(env, jvalues, 0, kRO_JNIAccess); + + SkRuntimeShaderBuilder::BuilderUniform uniform = builder->uniform(name.c_str()); + if (uniform.fVar == nullptr) { + ThrowIAEFmt(env, "unable to find uniform named %s", name.c_str()); + } else if (!uniform.set<float>(autoValues.ptr(), autoValues.length())) { + ThrowIAEFmt(env, "mismatch in byte size for uniform [expected: %zu actual: %zu]", + uniform.fVar->sizeInBytes(), sizeof(float) * autoValues.length()); + } } -/////////////////////////////////////////////////////////////////////////////////////////////// +static void RuntimeShader_updateShader(JNIEnv* env, jobject, jlong shaderBuilder, + jstring jUniformName, jlong shaderHandle) { + SkRuntimeShaderBuilder* builder = reinterpret_cast<SkRuntimeShaderBuilder*>(shaderBuilder); + ScopedUtfChars name(env, jUniformName); + SkShader* shader = reinterpret_cast<SkShader*>(shaderHandle); -static void Effect_safeUnref(SkRuntimeEffect* effect) { - SkSafeUnref(effect); -} + SkRuntimeShaderBuilder::BuilderChild child = builder->child(name.c_str()); + if (child.fIndex == -1) { + ThrowIAEFmt(env, "unable to find shader named %s", name.c_str()); + return; + } -static jlong RuntimeShader_getNativeFinalizer(JNIEnv*, jobject) { - return static_cast<jlong>(reinterpret_cast<uintptr_t>(&Effect_safeUnref)); + builder->child(name.c_str()) = sk_ref_sp(shader); } /////////////////////////////////////////////////////////////////////////////////////////////// @@ -256,7 +316,7 @@ static const JNINativeMethod gShaderMethods[] = { }; static const JNINativeMethod gBitmapShaderMethods[] = { - { "nativeCreate", "(JJII)J", (void*)BitmapShader_constructor }, + { "nativeCreate", "(JJIIZ)J", (void*)BitmapShader_constructor }, }; static const JNINativeMethod gLinearGradientMethods[] = { @@ -264,7 +324,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[] = { @@ -276,10 +336,11 @@ static const JNINativeMethod gComposeShaderMethods[] = { }; static const JNINativeMethod gRuntimeShaderMethods[] = { - { "nativeGetFinalizer", "()J", (void*)RuntimeShader_getNativeFinalizer }, - { "nativeCreate", "(JJ[BJZ)J", (void*)RuntimeShader_create }, - { "nativeCreateShaderFactory", "(Ljava/lang/String;)J", - (void*)RuntimeShader_createShaderFactory }, + {"nativeGetFinalizer", "()J", (void*)RuntimeShader_getNativeFinalizer}, + {"nativeCreateShader", "(JJZ)J", (void*)RuntimeShader_create}, + {"nativeCreateBuilder", "(Ljava/lang/String;)J", (void*)RuntimeShader_createShaderBuilder}, + {"nativeUpdateUniforms", "(JLjava/lang/String;[F)V", (void*)RuntimeShader_updateUniforms}, + {"nativeUpdateShader", "(JLjava/lang/String;J)V", (void*)RuntimeShader_updateShader}, }; int register_android_graphics_Shader(JNIEnv* env) diff --git a/libs/hwui/jni/Typeface.cpp b/libs/hwui/jni/Typeface.cpp index 2a5f402a4fa6..8f455fe4ab43 100644 --- a/libs/hwui/jni/Typeface.cpp +++ b/libs/hwui/jni/Typeface.cpp @@ -14,16 +14,30 @@ * limitations under the License. */ +#define ATRACE_TAG ATRACE_TAG_VIEW #include "FontUtils.h" #include "GraphicsJNI.h" +#include "fonts/Font.h" #include <nativehelper/ScopedPrimitiveArray.h> #include <nativehelper/ScopedUtfChars.h> +#include "SkData.h" #include "SkTypeface.h" #include <hwui/Typeface.h> +#include <minikin/FontCollection.h> #include <minikin/FontFamily.h> +#include <minikin/FontFileParser.h> #include <minikin/SystemFonts.h> +#include <utils/TraceUtils.h> + +#include <mutex> +#include <unordered_map> + +#ifdef __ANDROID__ +#include <sys/stat.h> +#endif using namespace android; +using android::uirenderer::TraceUtils; static inline Typeface* toTypeface(jlong ptr) { return reinterpret_cast<Typeface*>(ptr); @@ -91,13 +105,27 @@ static jint Typeface_getWeight(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) { } static jlong Typeface_createFromArray(JNIEnv *env, jobject, jlongArray familyArray, - int weight, int italic) { + jlong fallbackPtr, int weight, int italic) { ScopedLongArrayRO families(env, familyArray); std::vector<std::shared_ptr<minikin::FontFamily>> familyVec; - familyVec.reserve(families.size()); - for (size_t i = 0; i < families.size(); i++) { - FontFamilyWrapper* family = reinterpret_cast<FontFamilyWrapper*>(families[i]); - familyVec.emplace_back(family->family); + Typeface* typeface = (fallbackPtr == 0) ? nullptr : toTypeface(fallbackPtr); + if (typeface != nullptr) { + const std::vector<std::shared_ptr<minikin::FontFamily>>& fallbackFamilies = + toTypeface(fallbackPtr)->fFontCollection->getFamilies(); + familyVec.reserve(families.size() + fallbackFamilies.size()); + for (size_t i = 0; i < families.size(); i++) { + FontFamilyWrapper* family = reinterpret_cast<FontFamilyWrapper*>(families[i]); + familyVec.emplace_back(family->family); + } + for (size_t i = 0; i < fallbackFamilies.size(); i++) { + familyVec.emplace_back(fallbackFamilies[i]); + } + } else { + familyVec.reserve(families.size()); + for (size_t i = 0; i < families.size(); i++) { + FontFamilyWrapper* family = reinterpret_cast<FontFamilyWrapper*>(families[i]); + familyVec.emplace_back(family->family); + } } return toJLong(Typeface::createFromFamilies(std::move(familyVec), weight, italic)); } @@ -132,6 +160,202 @@ static void Typeface_registerGenericFamily(JNIEnv *env, jobject, jstring familyN toTypeface(ptr)->fFontCollection); } +#ifdef __ANDROID__ + +static bool getVerity(const std::string& path) { + struct statx out = {}; + if (statx(AT_FDCWD, path.c_str(), 0 /* flags */, STATX_ALL, &out) != 0) { + ALOGE("statx failed for %s, errno = %d", path.c_str(), errno); + return false; + } + + // Validity check. + if ((out.stx_attributes_mask & STATX_ATTR_VERITY) == 0) { + // STATX_ATTR_VERITY not supported by kernel. + return false; + } + + return (out.stx_attributes & STATX_ATTR_VERITY) != 0; +} + +#else + +static bool getVerity(const std::string&) { + // verity check is not enabled on desktop. + return false; +} + +#endif // __ANDROID__ + +static sk_sp<SkData> makeSkDataCached(const std::string& path, bool hasVerity) { + // We don't clear cache as Typeface objects created by Typeface_readTypefaces() will be stored + // in a static field and will not be garbage collected. + static std::unordered_map<std::string, sk_sp<SkData>> cache; + static std::mutex mutex; + ALOG_ASSERT(!path.empty()); + if (hasVerity && !getVerity(path)) { + LOG_ALWAYS_FATAL("verity bit was removed from %s", path.c_str()); + return nullptr; + } + std::lock_guard lock{mutex}; + sk_sp<SkData>& entry = cache[path]; + if (entry.get() == nullptr) { + entry = SkData::MakeFromFileName(path.c_str()); + } + return entry; +} + +static std::function<std::shared_ptr<minikin::MinikinFont>()> readMinikinFontSkia( + minikin::BufferReader* reader) { + const void* buffer = reader->data(); + size_t pos = reader->pos(); + // Advance reader's position. + reader->skipString(); // fontPath + reader->skip<int>(); // fontIndex + reader->skipArray<minikin::FontVariation>(); // axesPtr, axesCount + bool hasVerity = static_cast<bool>(reader->read<int8_t>()); + if (hasVerity) { + reader->skip<uint32_t>(); // expectedFontRevision + reader->skipString(); // expectedPostScriptName + } + return [buffer, pos]() -> std::shared_ptr<minikin::MinikinFont> { + minikin::BufferReader fontReader(buffer, pos); + std::string_view fontPath = fontReader.readString(); + std::string path(fontPath.data(), fontPath.size()); + ATRACE_FORMAT("Loading font %s", path.c_str()); + int fontIndex = fontReader.read<int>(); + const minikin::FontVariation* axesPtr; + uint32_t axesCount; + std::tie(axesPtr, axesCount) = fontReader.readArray<minikin::FontVariation>(); + bool hasVerity = static_cast<bool>(fontReader.read<int8_t>()); + uint32_t expectedFontRevision; + std::string_view expectedPostScriptName; + if (hasVerity) { + expectedFontRevision = fontReader.read<uint32_t>(); + expectedPostScriptName = fontReader.readString(); + } + sk_sp<SkData> data = makeSkDataCached(path, hasVerity); + if (data.get() == nullptr) { + // This may happen if: + // 1. When the process failed to open the file (e.g. invalid path or permission). + // 2. When the process failed to map the file (e.g. hitting max_map_count limit). + ALOGE("Failed to make SkData from file name: %s", path.c_str()); + return nullptr; + } + const void* fontPtr = data->data(); + size_t fontSize = data->size(); + if (hasVerity) { + // Verify font metadata if verity is enabled. + minikin::FontFileParser parser(fontPtr, fontSize, fontIndex); + std::optional<uint32_t> revision = parser.getFontRevision(); + if (!revision.has_value() || revision.value() != expectedFontRevision) { + LOG_ALWAYS_FATAL("Wrong font revision: %s", path.c_str()); + return nullptr; + } + std::optional<std::string> psName = parser.getPostScriptName(); + if (!psName.has_value() || psName.value() != expectedPostScriptName) { + LOG_ALWAYS_FATAL("Wrong PostScript name: %s", path.c_str()); + return nullptr; + } + } + std::vector<minikin::FontVariation> axes(axesPtr, axesPtr + axesCount); + std::shared_ptr<minikin::MinikinFont> minikinFont = + fonts::createMinikinFontSkia(std::move(data), fontPath, fontPtr, fontSize, + fontIndex, axes); + if (minikinFont == nullptr) { + ALOGE("Failed to create MinikinFontSkia: %s", path.c_str()); + return nullptr; + } + return minikinFont; + }; +} + +static void writeMinikinFontSkia(minikin::BufferWriter* writer, + const minikin::MinikinFont* typeface) { + const std::string& path = typeface->GetFontPath(); + writer->writeString(path); + writer->write<int>(typeface->GetFontIndex()); + const std::vector<minikin::FontVariation>& axes = typeface->GetAxes(); + writer->writeArray<minikin::FontVariation>(axes.data(), axes.size()); + bool hasVerity = getVerity(path); + writer->write<int8_t>(static_cast<int8_t>(hasVerity)); + if (hasVerity) { + // Write font metadata for verification only when verity is enabled. + minikin::FontFileParser parser(typeface->GetFontData(), typeface->GetFontSize(), + typeface->GetFontIndex()); + std::optional<uint32_t> revision = parser.getFontRevision(); + LOG_ALWAYS_FATAL_IF(!revision.has_value()); + writer->write<uint32_t>(revision.value()); + std::optional<std::string> psName = parser.getPostScriptName(); + LOG_ALWAYS_FATAL_IF(!psName.has_value()); + writer->writeString(psName.value()); + } +} + +static jint Typeface_writeTypefaces(JNIEnv *env, jobject, jobject buffer, jlongArray faceHandles) { + ScopedLongArrayRO faces(env, faceHandles); + std::vector<Typeface*> typefaces; + typefaces.reserve(faces.size()); + for (size_t i = 0; i < faces.size(); i++) { + typefaces.push_back(toTypeface(faces[i])); + } + void* addr = buffer == nullptr ? nullptr : env->GetDirectBufferAddress(buffer); + minikin::BufferWriter writer(addr); + std::vector<std::shared_ptr<minikin::FontCollection>> fontCollections; + std::unordered_map<std::shared_ptr<minikin::FontCollection>, size_t> fcToIndex; + for (Typeface* typeface : typefaces) { + bool inserted = fcToIndex.emplace(typeface->fFontCollection, fontCollections.size()).second; + if (inserted) { + fontCollections.push_back(typeface->fFontCollection); + } + } + minikin::FontCollection::writeVector<writeMinikinFontSkia>(&writer, fontCollections); + writer.write<uint32_t>(typefaces.size()); + for (Typeface* typeface : typefaces) { + writer.write<uint32_t>(fcToIndex.find(typeface->fFontCollection)->second); + typeface->fStyle.writeTo(&writer); + writer.write<Typeface::Style>(typeface->fAPIStyle); + writer.write<int>(typeface->fBaseWeight); + } + return static_cast<jint>(writer.size()); +} + +static jlongArray Typeface_readTypefaces(JNIEnv *env, jobject, jobject buffer) { + void* addr = buffer == nullptr ? nullptr : env->GetDirectBufferAddress(buffer); + if (addr == nullptr) return nullptr; + minikin::BufferReader reader(addr); + std::vector<std::shared_ptr<minikin::FontCollection>> fontCollections = + minikin::FontCollection::readVector<readMinikinFontSkia>(&reader); + uint32_t typefaceCount = reader.read<uint32_t>(); + std::vector<jlong> faceHandles; + faceHandles.reserve(typefaceCount); + for (uint32_t i = 0; i < typefaceCount; i++) { + Typeface* typeface = new Typeface; + typeface->fFontCollection = fontCollections[reader.read<uint32_t>()]; + typeface->fStyle = minikin::FontStyle(&reader); + typeface->fAPIStyle = reader.read<Typeface::Style>(); + typeface->fBaseWeight = reader.read<int>(); + faceHandles.push_back(toJLong(typeface)); + } + const jlongArray result = env->NewLongArray(typefaceCount); + env->SetLongArrayRegion(result, 0, typefaceCount, faceHandles.data()); + return result; +} + + +static void Typeface_forceSetStaticFinalField(JNIEnv *env, jclass cls, jstring fieldName, + jobject typeface) { + ScopedUtfChars fieldNameChars(env, fieldName); + jfieldID fid = + env->GetStaticFieldID(cls, fieldNameChars.c_str(), "Landroid/graphics/Typeface;"); + if (fid == 0) { + jniThrowRuntimeException(env, "Unable to find field"); + return; + } + env->SetStaticObjectField(cls, fid, typeface); +} + + /////////////////////////////////////////////////////////////////////////////// static const JNINativeMethod gTypefaceMethods[] = { @@ -144,12 +368,16 @@ static const JNINativeMethod gTypefaceMethods[] = { { "nativeGetReleaseFunc", "()J", (void*)Typeface_getReleaseFunc }, { "nativeGetStyle", "(J)I", (void*)Typeface_getStyle }, { "nativeGetWeight", "(J)I", (void*)Typeface_getWeight }, - { "nativeCreateFromArray", "([JII)J", + { "nativeCreateFromArray", "([JJII)J", (void*)Typeface_createFromArray }, { "nativeSetDefault", "(J)V", (void*)Typeface_setDefault }, { "nativeGetSupportedAxes", "(J)[I", (void*)Typeface_getSupportedAxes }, { "nativeRegisterGenericFamily", "(Ljava/lang/String;J)V", (void*)Typeface_registerGenericFamily }, + { "nativeWriteTypefaces", "(Ljava/nio/ByteBuffer;[J)I", (void*)Typeface_writeTypefaces}, + { "nativeReadTypefaces", "(Ljava/nio/ByteBuffer;)[J", (void*)Typeface_readTypefaces}, + { "nativeForceSetStaticFinalField", "(Ljava/lang/String;Landroid/graphics/Typeface;)V", + (void*)Typeface_forceSetStaticFinalField }, }; int register_android_graphics_Typeface(JNIEnv* env) 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..89fb8bb2a2a0 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" @@ -92,16 +93,14 @@ static jint save(CRITICAL_JNI_PARAMS_COMMA jlong canvasHandle, jint flagsHandle) } static jint saveLayer(CRITICAL_JNI_PARAMS_COMMA jlong canvasHandle, jfloat l, jfloat t, - jfloat r, jfloat b, jlong paintHandle, jint flagsHandle) { + jfloat r, jfloat b, jlong paintHandle) { Paint* paint = reinterpret_cast<Paint*>(paintHandle); - SaveFlags::Flags flags = static_cast<SaveFlags::Flags>(flagsHandle); - return static_cast<jint>(get_canvas(canvasHandle)->saveLayer(l, t, r, b, paint, flags)); + return static_cast<jint>(get_canvas(canvasHandle)->saveLayer(l, t, r, b, paint)); } static jint saveLayerAlpha(CRITICAL_JNI_PARAMS_COMMA jlong canvasHandle, jfloat l, jfloat t, - jfloat r, jfloat b, jint alpha, jint flagsHandle) { - SaveFlags::Flags flags = static_cast<SaveFlags::Flags>(flagsHandle); - return static_cast<jint>(get_canvas(canvasHandle)->saveLayerAlpha(l, t, r, b, alpha, flags)); + jfloat r, jfloat b, jint alpha) { + return static_cast<jint>(get_canvas(canvasHandle)->saveLayerAlpha(l, t, r, b, alpha)); } static jint saveUnclippedLayer(CRITICAL_JNI_PARAMS_COMMA jlong canvasHandle, jint l, jint t, jint r, jint b) { @@ -540,6 +539,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) { @@ -672,8 +686,8 @@ static const JNINativeMethod gMethods[] = { {"nGetWidth","(J)I", (void*) CanvasJNI::getWidth}, {"nGetHeight","(J)I", (void*) CanvasJNI::getHeight}, {"nSave","(JI)I", (void*) CanvasJNI::save}, - {"nSaveLayer","(JFFFFJI)I", (void*) CanvasJNI::saveLayer}, - {"nSaveLayerAlpha","(JFFFFII)I", (void*) CanvasJNI::saveLayerAlpha}, + {"nSaveLayer","(JFFFFJ)I", (void*) CanvasJNI::saveLayer}, + {"nSaveLayerAlpha","(JFFFFI)I", (void*) CanvasJNI::saveLayerAlpha}, {"nSaveUnclippedLayer","(JIIII)I", (void*) CanvasJNI::saveUnclippedLayer}, {"nRestoreUnclippedLayer","(JIJ)V", (void*) CanvasJNI::restoreUnclippedLayer}, {"nGetSaveCount","(J)I", (void*) CanvasJNI::getSaveCount}, @@ -719,6 +733,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..855d56ee2e55 100644 --- a/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp +++ b/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp @@ -20,8 +20,8 @@ #include <utils/Looper.h> #endif -#include <SkBitmap.h> #include <SkRegion.h> +#include <SkRuntimeEffect.h> #include <Rect.h> #include <RenderNode.h> @@ -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,15 +92,17 @@ 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) { +static void android_view_DisplayListCanvas_finishRecording( + CRITICAL_JNI_PARAMS_COMMA jlong canvasPtr, jlong renderNodePtr) { Canvas* canvas = reinterpret_cast<Canvas*>(canvasPtr); - return reinterpret_cast<jlong>(canvas->finishRecording()); + RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr); + canvas->finishRecording(renderNode); } static void android_view_DisplayListCanvas_drawRenderNode(CRITICAL_JNI_PARAMS_COMMA jlong canvasPtr, jlong renderNodePtr) { @@ -171,6 +141,22 @@ static void android_view_DisplayListCanvas_drawCircleProps(CRITICAL_JNI_PARAMS_C canvas->drawCircle(xProp, yProp, radiusProp, paintProp); } +static void android_view_DisplayListCanvas_drawRippleProps(CRITICAL_JNI_PARAMS_COMMA jlong canvasPtr, + jlong xPropPtr, jlong yPropPtr, + jlong radiusPropPtr, jlong paintPropPtr, + jlong progressPropPtr, + jlong builderPtr) { + Canvas* canvas = reinterpret_cast<Canvas*>(canvasPtr); + CanvasPropertyPrimitive* xProp = reinterpret_cast<CanvasPropertyPrimitive*>(xPropPtr); + CanvasPropertyPrimitive* yProp = reinterpret_cast<CanvasPropertyPrimitive*>(yPropPtr); + CanvasPropertyPrimitive* radiusProp = reinterpret_cast<CanvasPropertyPrimitive*>(radiusPropPtr); + CanvasPropertyPaint* paintProp = reinterpret_cast<CanvasPropertyPaint*>(paintPropPtr); + CanvasPropertyPrimitive* progressProp = + reinterpret_cast<CanvasPropertyPrimitive*>(progressPropPtr); + SkRuntimeShaderBuilder* builder = reinterpret_cast<SkRuntimeShaderBuilder*>(builderPtr); + canvas->drawRipple(xProp, yProp, radiusProp, paintProp, progressProp, *builder); +} + static void android_view_DisplayListCanvas_drawWebViewFunctor(CRITICAL_JNI_PARAMS_COMMA jlong canvasPtr, jint functor) { Canvas* canvas = reinterpret_cast<Canvas*>(canvasPtr); canvas->drawWebViewFunctor(functor); @@ -183,24 +169,19 @@ 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 }, - { "nFinishRecording", "(J)J", (void*) android_view_DisplayListCanvas_finishRecording }, + { "nEnableZ", "(JZ)V", (void*) android_view_DisplayListCanvas_enableZ }, + { "nFinishRecording", "(JJ)V", (void*) android_view_DisplayListCanvas_finishRecording }, { "nDrawRenderNode", "(JJ)V", (void*) android_view_DisplayListCanvas_drawRenderNode }, { "nDrawTextureLayer", "(JJ)V", (void*) android_view_DisplayListCanvas_drawTextureLayer }, { "nDrawCircle", "(JJJJJ)V", (void*) android_view_DisplayListCanvas_drawCircleProps }, { "nDrawRoundRect", "(JJJJJJJJ)V",(void*) android_view_DisplayListCanvas_drawRoundRectProps }, { "nDrawWebViewFunctor", "(JI)V", (void*) android_view_DisplayListCanvas_drawWebViewFunctor }, + { "nDrawRipple", "(JJJJJJJ)V", (void*) android_view_DisplayListCanvas_drawRippleProps }, }; int register_android_view_DisplayListCanvas(JNIEnv* env) { 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..80239687a7fb 100644 --- a/libs/hwui/jni/android_graphics_RenderNode.cpp +++ b/libs/hwui/jni/android_graphics_RenderNode.cpp @@ -76,11 +76,9 @@ static jlong android_view_RenderNode_getNativeFinalizer(JNIEnv* env, return static_cast<jlong>(reinterpret_cast<uintptr_t>(&releaseRenderNode)); } -static void android_view_RenderNode_setDisplayList(JNIEnv* env, - jobject clazz, jlong renderNodePtr, jlong displayListPtr) { +static void android_view_RenderNode_discardDisplayList(CRITICAL_JNI_PARAMS_COMMA jlong renderNodePtr) { RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr); - DisplayList* newData = reinterpret_cast<DisplayList*>(displayListPtr); - renderNode->setStagingDisplayList(newData); + renderNode->discardStagingDisplayList(); } static jboolean android_view_RenderNode_isValid(CRITICAL_JNI_PARAMS_COMMA jlong renderNodePtr) { @@ -168,6 +166,31 @@ static jboolean android_view_RenderNode_setOutlineNone(CRITICAL_JNI_PARAMS_COMMA return true; } +static jboolean android_view_RenderNode_clearStretch(CRITICAL_JNI_PARAMS_COMMA jlong renderNodePtr) { + RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr); + auto& stretch = renderNode->mutateStagingProperties() + .mutateLayerProperties().mutableStretchEffect(); + if (stretch.isEmpty()) { + return false; + } + stretch.setEmpty(); + renderNode->setPropertyFieldsDirty(RenderNode::GENERIC); + return true; +} + +static jboolean android_view_RenderNode_stretch(CRITICAL_JNI_PARAMS_COMMA jlong renderNodePtr, + jfloat left, jfloat top, jfloat right, jfloat bottom, jfloat vX, jfloat vY, jfloat max) { + RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr); + renderNode->mutateStagingProperties().mutateLayerProperties().mutableStretchEffect().mergeWith( + StretchEffect{ + .stretchArea = SkRect::MakeLTRB(left, top, right, bottom), + .stretchDirection = {.fX = vX, .fY = vY}, + .maxStretchAmount = max + }); + renderNode->setPropertyFieldsDirty(RenderNode::GENERIC); + return true; +} + static jboolean android_view_RenderNode_hasShadow(CRITICAL_JNI_PARAMS_COMMA jlong renderNodePtr) { RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr); return renderNode->stagingProperties().hasShadow(); @@ -215,6 +238,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, @@ -651,18 +680,11 @@ static const JNINativeMethod gMethods[] = { { "nAddAnimator", "(JJ)V", (void*) android_view_RenderNode_addAnimator }, { "nEndAllAnimators", "(J)V", (void*) android_view_RenderNode_endAllAnimators }, { "nRequestPositionUpdates", "(JLandroid/graphics/RenderNode$PositionUpdateListener;)V", (void*) android_view_RenderNode_requestPositionUpdates }, - { "nSetDisplayList", "(JJ)V", (void*) android_view_RenderNode_setDisplayList }, - - -// ---------------------------------------------------------------------------- -// Fast JNI via @CriticalNative annotation in RenderNode.java -// ---------------------------------------------------------------------------- - { "nSetDisplayList", "(JJ)V", (void*) android_view_RenderNode_setDisplayList }, - // ---------------------------------------------------------------------------- // Critical JNI via @CriticalNative annotation in RenderNode.java // ---------------------------------------------------------------------------- + { "nDiscardDisplayList", "(J)V", (void*) android_view_RenderNode_discardDisplayList }, { "nIsValid", "(J)Z", (void*) android_view_RenderNode_isValid }, { "nSetLayerType", "(JI)Z", (void*) android_view_RenderNode_setLayerType }, { "nGetLayerType", "(J)I", (void*) android_view_RenderNode_getLayerType }, @@ -681,6 +703,8 @@ static const JNINativeMethod gMethods[] = { { "nSetOutlinePath", "(JJF)Z", (void*) android_view_RenderNode_setOutlinePath }, { "nSetOutlineEmpty", "(J)Z", (void*) android_view_RenderNode_setOutlineEmpty }, { "nSetOutlineNone", "(J)Z", (void*) android_view_RenderNode_setOutlineNone }, + { "nClearStretch", "(J)Z", (void*) android_view_RenderNode_clearStretch }, + { "nStretch", "(JFFFFFFF)Z", (void*) android_view_RenderNode_stretch }, { "nHasShadow", "(J)Z", (void*) android_view_RenderNode_hasShadow }, { "nSetSpotShadowColor", "(JI)Z", (void*) android_view_RenderNode_setSpotShadowColor }, { "nGetSpotShadowColor", "(J)I", (void*) android_view_RenderNode_getSpotShadowColor }, @@ -690,6 +714,7 @@ static const JNINativeMethod gMethods[] = { { "nSetRevealClip", "(JZFFF)Z", (void*) android_view_RenderNode_setRevealClip }, { "nSetAlpha", "(JF)Z", (void*) android_view_RenderNode_setAlpha }, + { "nSetRenderEffect", "(JJ)Z", (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..b944310d8822 100644 --- a/libs/hwui/jni/fonts/Font.cpp +++ b/libs/hwui/jni/fonts/Font.cpp @@ -17,7 +17,10 @@ #undef LOG_TAG #define LOG_TAG "Minikin" +#include "Font.h" #include "SkData.h" +#include "SkFont.h" +#include "SkFontMetrics.h" #include "SkFontMgr.h" #include "SkRefCnt.h" #include "SkTypeface.h" @@ -27,8 +30,10 @@ #include "FontUtils.h" #include <hwui/MinikinSkia.h> +#include <hwui/Paint.h> #include <hwui/Typeface.h> #include <minikin/FontFamily.h> +#include <minikin/FontFileParser.h> #include <ui/FatVector.h> #include <memory> @@ -92,34 +97,51 @@ static jlong Font_Builder_build(JNIEnv* env, jobject clazz, jlong builderPtr, jo jobject fontRef = MakeGlobalRefOrDie(env, buffer); sk_sp<SkData> data(SkData::MakeWithProc(fontPtr, fontSize, release_global_ref, reinterpret_cast<void*>(fontRef))); - - FatVector<SkFontArguments::Axis, 2> skiaAxes; - for (const auto& axis : builder->axes) { - skiaAxes.emplace_back(SkFontArguments::Axis{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()); - - sk_sp<SkFontMgr> fm(SkFontMgr::RefDefault()); - sk_sp<SkTypeface> face(fm->makeFromStream(std::move(fontData), params)); - if (face == nullptr) { + std::shared_ptr<minikin::MinikinFont> minikinFont = fonts::createMinikinFontSkia( + std::move(data), std::string_view(fontPath.c_str(), fontPath.size()), + fontPtr, fontSize, ttcIndex, builder->axes); + if (minikinFont == nullptr) { jniThrowException(env, "java/lang/IllegalArgumentException", "Failed to create internal object. maybe invalid font data."); return 0; } - std::shared_ptr<minikin::MinikinFont> minikinFont = - 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 +149,223 @@ 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_getNativeFontPtr(CRITICAL_JNI_PARAMS_COMMA jlong fontHandle) { + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontHandle); + return reinterpret_cast<jlong>(font->font.get()); +} + +// Critical Native +static jlong Font_GetBufferAddress(CRITICAL_JNI_PARAMS_COMMA jlong fontHandle) { + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontHandle); + const void* bufferPtr = font->font->typeface()->GetFontData(); + return reinterpret_cast<jlong>(bufferPtr); +} + +/////////////////////////////////////////////////////////////////////////////// + +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); +} + +/////////////////////////////////////////////////////////////////////////////// + +// Fast Native +static jlong FontFileUtil_getFontRevision(JNIEnv* env, jobject, jobject buffer, jint index) { + NPE_CHECK_RETURN_ZERO(env, buffer); + const void* fontPtr = env->GetDirectBufferAddress(buffer); + if (fontPtr == nullptr) { + jniThrowException(env, "java/lang/IllegalArgumentException", "Not a direct buffer"); + return 0; + } + jlong fontSize = env->GetDirectBufferCapacity(buffer); + if (fontSize <= 0) { + jniThrowException(env, "java/lang/IllegalArgumentException", + "buffer size must not be zero or negative"); + return 0; + } + minikin::FontFileParser parser(fontPtr, fontSize, index); + std::optional<uint32_t> revision = parser.getFontRevision(); + if (!revision.has_value()) { + return -1L; + } + return revision.value(); +} + +static jstring FontFileUtil_getFontPostScriptName(JNIEnv* env, jobject, jobject buffer, + jint index) { + NPE_CHECK_RETURN_ZERO(env, buffer); + const void* fontPtr = env->GetDirectBufferAddress(buffer); + if (fontPtr == nullptr) { + jniThrowException(env, "java/lang/IllegalArgumentException", "Not a direct buffer"); + return nullptr; + } + jlong fontSize = env->GetDirectBufferCapacity(buffer); + if (fontSize <= 0) { + jniThrowException(env, "java/lang/IllegalArgumentException", + "buffer size must not be zero or negative"); + return nullptr; + } + minikin::FontFileParser parser(fontPtr, fontSize, index); + std::optional<std::string> psName = parser.getPostScriptName(); + if (!psName.has_value()) { + return nullptr; // null + } + return env->NewStringUTF(psName->c_str()); +} + +static jint FontFileUtil_isPostScriptType1Font(JNIEnv* env, jobject, jobject buffer, jint index) { + NPE_CHECK_RETURN_ZERO(env, buffer); + const void* fontPtr = env->GetDirectBufferAddress(buffer); + if (fontPtr == nullptr) { + jniThrowException(env, "java/lang/IllegalArgumentException", "Not a direct buffer"); + return -1; + } + jlong fontSize = env->GetDirectBufferCapacity(buffer); + if (fontSize <= 0) { + jniThrowException(env, "java/lang/IllegalArgumentException", + "buffer size must not be zero or negative"); + return -1; + } + minikin::FontFileParser parser(fontPtr, fontSize, index); + std::optional<bool> isType1 = parser.isPostScriptType1Font(); + if (!isType1.has_value()) { + return -1; // not an OpenType font. HarfBuzz failed to parse it. + } + return isType1.value(); +} + +/////////////////////////////////////////////////////////////////////////////// + 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 }, + { "nGetNativeFontPtr", "(J)J", (void*) Font_getNativeFontPtr }, + { "nGetFontBufferAddress", "(J)J", (void*) Font_GetBufferAddress }, +}; + +static const JNINativeMethod gFontBufferHelperMethods[] = { + { "nRefFontBuffer", "(J)J", (void*) FontBufferHelper_refFontBuffer }, + { "nWrapByteBuffer", "(J)Ljava/nio/ByteBuffer;", (void*) FontBufferHelper_wrapByteBuffer }, + { "nGetReleaseFunc", "()J", (void*) FontBufferHelper_getReleaseFunc }, +}; + +static const JNINativeMethod gFontFileUtilMethods[] = { + { "nGetFontRevision", "(Ljava/nio/ByteBuffer;I)J", (void*) FontFileUtil_getFontRevision }, + { "nGetFontPostScriptName", "(Ljava/nio/ByteBuffer;I)Ljava/lang/String;", + (void*) FontFileUtil_getFontPostScriptName }, + { "nIsPostScriptType1Font", "(Ljava/nio/ByteBuffer;I)I", + (void*) FontFileUtil_isPostScriptType1Font }, +}; + 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)) + + RegisterMethodsOrDie(env, "android/graphics/fonts/FontFileUtil", gFontFileUtilMethods, + NELEM(gFontFileUtilMethods)); } +namespace fonts { + +std::shared_ptr<minikin::MinikinFont> createMinikinFontSkia( + sk_sp<SkData>&& data, std::string_view fontPath, const void *fontPtr, size_t fontSize, + int ttcIndex, const std::vector<minikin::FontVariation>& axes) { + FatVector<SkFontArguments::VariationPosition::Coordinate, 2> skVariation; + for (const auto& axis : axes) { + skVariation.push_back({axis.axisTag, axis.value}); + } + + std::unique_ptr<SkStreamAsset> fontData(new SkMemoryStream(std::move(data))); + + 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), args)); + if (face == nullptr) { + return nullptr; + } + return std::make_shared<MinikinFontSkia>(std::move(face), fontPtr, fontSize, + fontPath, ttcIndex, axes); } + +} // namespace fonts + +} // namespace android diff --git a/libs/hwui/jni/fonts/Font.h b/libs/hwui/jni/fonts/Font.h new file mode 100644 index 000000000000..b5d20bf8cc3c --- /dev/null +++ b/libs/hwui/jni/fonts/Font.h @@ -0,0 +1,40 @@ +/* + * 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. + */ +#ifndef FONTS_FONT_H_ +#define FONTS_FONT_H_ + +#include <minikin/FontVariation.h> +#include <minikin/MinikinFont.h> +#include <SkRefCnt.h> + +#include <string_view> +#include <vector> + +class SkData; + +namespace android { + +namespace fonts { + +std::shared_ptr<minikin::MinikinFont> createMinikinFontSkia( + sk_sp<SkData>&& data, std::string_view fontPath, const void *fontPtr, size_t fontSize, + int ttcIndex, const std::vector<minikin::FontVariation>& axes); + +} // namespace fonts + +} // namespace android + +#endif /* FONTS_FONT_H_ */ 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/fonts/NativeFont.cpp b/libs/hwui/jni/fonts/NativeFont.cpp new file mode 100644 index 000000000000..c5c5d464ccac --- /dev/null +++ b/libs/hwui/jni/fonts/NativeFont.cpp @@ -0,0 +1,125 @@ +/* + * 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. + */ + +#undef LOG_TAG +#define LOG_TAG "Minikin" + +#include "Font.h" +#include "SkData.h" +#include "SkFont.h" +#include "SkFontMetrics.h" +#include "SkFontMgr.h" +#include "SkRefCnt.h" +#include "SkTypeface.h" +#include "GraphicsJNI.h" +#include <nativehelper/ScopedUtfChars.h> +#include "Utils.h" +#include "FontUtils.h" + +#include <hwui/MinikinSkia.h> +#include <hwui/Paint.h> +#include <hwui/Typeface.h> +#include <minikin/FontFamily.h> +#include <minikin/LocaleList.h> +#include <ui/FatVector.h> + +#include <memory> + +namespace android { + +// Critical Native +static jint NativeFont_getFamilyCount(CRITICAL_JNI_PARAMS_COMMA jlong typefaceHandle) { + Typeface* tf = reinterpret_cast<Typeface*>(typefaceHandle); + return tf->fFontCollection->getFamilies().size(); +} + +// Critical Native +static jlong NativeFont_getFamily(CRITICAL_JNI_PARAMS_COMMA jlong typefaceHandle, jint index) { + Typeface* tf = reinterpret_cast<Typeface*>(typefaceHandle); + return reinterpret_cast<jlong>(tf->fFontCollection->getFamilies()[index].get()); + +} + +// Fast Native +static jstring NativeFont_getLocaleList(JNIEnv* env, jobject, jlong familyHandle) { + minikin::FontFamily* family = reinterpret_cast<minikin::FontFamily*>(familyHandle); + uint32_t localeListId = family->localeListId(); + return env->NewStringUTF(minikin::getLocaleString(localeListId).c_str()); +} + +// Critical Native +static jint NativeFont_getFontCount(CRITICAL_JNI_PARAMS_COMMA jlong familyHandle) { + minikin::FontFamily* family = reinterpret_cast<minikin::FontFamily*>(familyHandle); + return family->getNumFonts(); +} + +// Critical Native +static jlong NativeFont_getFont(CRITICAL_JNI_PARAMS_COMMA jlong familyHandle, jint index) { + minikin::FontFamily* family = reinterpret_cast<minikin::FontFamily*>(familyHandle); + return reinterpret_cast<jlong>(family->getFont(index)); +} + +// Critical Native +static jlong NativeFont_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 NativeFont_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 NativeFont_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()); +} + +/////////////////////////////////////////////////////////////////////////////// + +static const JNINativeMethod gNativeFontMethods[] = { + { "nGetFamilyCount", "(J)I", (void*) NativeFont_getFamilyCount }, + { "nGetFamily", "(JI)J", (void*) NativeFont_getFamily }, + { "nGetLocaleList", "(J)Ljava/lang/String;", (void*) NativeFont_getLocaleList }, + { "nGetFontCount", "(J)I", (void*) NativeFont_getFontCount }, + { "nGetFont", "(JI)J", (void*) NativeFont_getFont }, + { "nGetFontInfo", "(J)J", (void*) NativeFont_getFontInfo }, + { "nGetAxisInfo", "(JI)J", (void*) NativeFont_getAxisInfo }, + { "nGetFontPath", "(J)Ljava/lang/String;", (void*) NativeFont_getFontPath }, +}; + +int register_android_graphics_fonts_NativeFont(JNIEnv* env) { + return RegisterMethodsOrDie(env, "android/graphics/fonts/NativeFont", gNativeFontMethods, + NELEM(gNativeFontMethods)); +} + +} // namespace android diff --git a/libs/hwui/jni/pdf/PdfEditor.cpp b/libs/hwui/jni/pdf/PdfEditor.cpp index e65921ac8e0a..427bafa1bd83 100644 --- a/libs/hwui/jni/pdf/PdfEditor.cpp +++ b/libs/hwui/jni/pdf/PdfEditor.cpp @@ -129,8 +129,8 @@ static void nativeSetTransformAndClip(JNIEnv* env, jclass thiz, jlong documentPt // PDF's coordinate system origin is left-bottom while in graphics it // is the top-left. So, translate the PDF coordinates to ours. - SkMatrix reflectOnX = SkMatrix::MakeScale(1, -1); - SkMatrix moveUp = SkMatrix::MakeTrans(0, FPDF_GetPageHeight(page)); + SkMatrix reflectOnX = SkMatrix::Scale(1, -1); + SkMatrix moveUp = SkMatrix::Translate(0, FPDF_GetPageHeight(page)); SkMatrix coordinateChange = SkMatrix::Concat(moveUp, reflectOnX); // Apply the transformation what was created in our coordinates. 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/AnimatedDrawables.h b/libs/hwui/pipeline/skia/AnimatedDrawables.h index bf19655825b3..78591450f10a 100644 --- a/libs/hwui/pipeline/skia/AnimatedDrawables.h +++ b/libs/hwui/pipeline/skia/AnimatedDrawables.h @@ -18,6 +18,7 @@ #include <SkCanvas.h> #include <SkDrawable.h> +#include <SkRuntimeEffect.h> #include <utils/RefBase.h> #include "CanvasProperty.h" @@ -54,6 +55,59 @@ private: sp<uirenderer::CanvasPropertyPaint> mPaint; }; +class AnimatedRipple : public SkDrawable { +public: + AnimatedRipple(uirenderer::CanvasPropertyPrimitive* x, uirenderer::CanvasPropertyPrimitive* y, + uirenderer::CanvasPropertyPrimitive* radius, + uirenderer::CanvasPropertyPaint* paint, + uirenderer::CanvasPropertyPrimitive* progress, + const SkRuntimeShaderBuilder& effectBuilder) + : mX(x) + , mY(y) + , mRadius(radius) + , mPaint(paint) + , mProgress(progress) + , mRuntimeEffectBuilder(effectBuilder) {} + +protected: + virtual SkRect onGetBounds() override { + const float x = mX->value; + const float y = mY->value; + const float radius = mRadius->value; + return SkRect::MakeLTRB(x - radius, y - radius, x + radius, y + radius); + } + virtual void onDraw(SkCanvas* canvas) override { + SkRuntimeShaderBuilder::BuilderUniform center = mRuntimeEffectBuilder.uniform("in_origin"); + if (center.fVar != nullptr) { + center = SkV2{mX->value, mY->value}; + } + + SkRuntimeShaderBuilder::BuilderUniform radiusU = + mRuntimeEffectBuilder.uniform("in_radius"); + if (radiusU.fVar != nullptr) { + radiusU = mRadius->value; + } + + SkRuntimeShaderBuilder::BuilderUniform progressU = + mRuntimeEffectBuilder.uniform("in_progress"); + if (progressU.fVar != nullptr) { + progressU = mProgress->value; + } + + SkPaint paint = mPaint->value; + paint.setShader(mRuntimeEffectBuilder.makeShader(nullptr, false)); + canvas->drawCircle(mX->value, mY->value, mRadius->value, paint); + } + +private: + sp<uirenderer::CanvasPropertyPrimitive> mX; + sp<uirenderer::CanvasPropertyPrimitive> mY; + sp<uirenderer::CanvasPropertyPrimitive> mRadius; + sp<uirenderer::CanvasPropertyPaint> mPaint; + sp<uirenderer::CanvasPropertyPrimitive> mProgress; + SkRuntimeShaderBuilder mRuntimeEffectBuilder; +}; + class AnimatedCircle : public SkDrawable { public: AnimatedCircle(uirenderer::CanvasPropertyPrimitive* x, uirenderer::CanvasPropertyPrimitive* y, diff --git a/libs/hwui/pipeline/skia/DumpOpsCanvas.h b/libs/hwui/pipeline/skia/DumpOpsCanvas.h index 0eb526af127a..3580bed45a1f 100644 --- a/libs/hwui/pipeline/skia/DumpOpsCanvas.h +++ b/libs/hwui/pipeline/skia/DumpOpsCanvas.h @@ -29,7 +29,7 @@ namespace skiapipeline { */ class DumpOpsCanvas : public SkCanvas { public: - DumpOpsCanvas(std::ostream& output, int level, SkiaDisplayList& displayList) + DumpOpsCanvas(std::ostream& output, int level, const SkiaDisplayList& displayList) : mOutput(output) , mLevel(level) , mDisplayList(displayList) @@ -86,22 +86,18 @@ protected: mOutput << mIdent << "drawTextBlob" << std::endl; } - void onDrawImage(const SkImage*, SkScalar dx, SkScalar dy, const SkPaint*) override { + void onDrawImage2(const SkImage*, SkScalar dx, SkScalar dy, const SkSamplingOptions&, + const SkPaint*) override { mOutput << mIdent << "drawImage" << std::endl; } - void onDrawImageNine(const SkImage*, const SkIRect& center, const SkRect& dst, - const SkPaint*) override { - mOutput << mIdent << "drawImageNine" << std::endl; - } - - void onDrawImageRect(const SkImage*, const SkRect*, const SkRect&, const SkPaint*, - SrcRectConstraint) override { + void onDrawImageRect2(const SkImage*, const SkRect&, const SkRect&, const SkSamplingOptions&, + const SkPaint*, SrcRectConstraint) override { mOutput << mIdent << "drawImageRect" << std::endl; } - void onDrawImageLattice(const SkImage*, const Lattice& lattice, const SkRect& dst, - const SkPaint*) override { + void onDrawImageLattice2(const SkImage*, const Lattice& lattice, const SkRect& dst, + SkFilterMode, const SkPaint*) override { mOutput << mIdent << "drawImageLattice" << std::endl; } @@ -131,7 +127,7 @@ protected: } private: - RenderNodeDrawable* getRenderNodeDrawable(SkDrawable* drawable) { + const RenderNodeDrawable* getRenderNodeDrawable(SkDrawable* drawable) { for (auto& child : mDisplayList.mChildNodes) { if (drawable == &child) { return &child; @@ -151,7 +147,7 @@ private: std::ostream& mOutput; int mLevel; - SkiaDisplayList& mDisplayList; + const SkiaDisplayList& mDisplayList; std::string mIdent; }; 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..71f533c3fc4f 100644 --- a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp @@ -15,31 +15,20 @@ */ #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" #include "RenderNode.h" #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 @@ -49,23 +38,18 @@ static void setScissor(int viewportHeight, const SkIRect& clip) { } static void GetFboDetails(SkCanvas* canvas, GLuint* outFboID, SkISize* outFboSize) { - GrRenderTargetContext* renderTargetContext = - canvas->internal_private_accessTopLayerRenderTargetContext(); - LOG_ALWAYS_FATAL_IF(!renderTargetContext, "Failed to retrieve GrRenderTargetContext"); - - GrRenderTarget* renderTarget = renderTargetContext->accessRenderTarget(); - LOG_ALWAYS_FATAL_IF(!renderTarget, "accessRenderTarget failed"); - + GrBackendRenderTarget renderTarget = canvas->topLayerBackendRenderTarget(); GrGLFramebufferInfo fboInfo; - LOG_ALWAYS_FATAL_IF(!renderTarget->getBackendRenderTarget().getGLFramebufferInfo(&fboInfo), + LOG_ALWAYS_FATAL_IF(!renderTarget.getGLFramebufferInfo(&fboInfo), "getGLFrameBufferInfo failed"); *outFboID = fboInfo.fFBOID; - *outFboSize = SkISize::Make(renderTargetContext->width(), renderTargetContext->height()); + *outFboSize = renderTarget.dimensions(); } 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 @@ -83,9 +67,9 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) { SkISize fboSize; GetFboDetails(canvas, &fboID, &fboSize); - SkIRect surfaceBounds = canvas->internal_private_getTopLayerBounds(); + SkIRect surfaceBounds = canvas->topLayerBounds(); SkIRect clipBounds = canvas->getDeviceClipBounds(); - SkM44 mat4(canvas->experimental_getLocalToDevice()); + SkM44 mat4(canvas->getLocalToDevice()); SkRegion clipRegion; canvas->temporary_internal_getRgnClip(&clipRegion); @@ -96,7 +80,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 +134,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 +170,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 +181,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) { @@ -214,7 +194,7 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) { canvas->concat(invertedMatrix); const SkIRect deviceBounds = canvas->getDeviceClipBounds(); - tmpSurface->draw(canvas, deviceBounds.fLeft, deviceBounds.fTop, nullptr); + tmpSurface->draw(canvas, deviceBounds.fLeft, deviceBounds.fTop); } } 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..34df5ddbb210 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")); @@ -137,18 +141,20 @@ bool LayerDrawable::DrawLayer(GrContext* context, SkCanvas* canvas, Layer* layer // then use nearest neighbor, otherwise use bilerp sampling. // Skia TextureOp has the above logic build-in, but not NonAAFillRectOp. TextureOp works // only for SrcOver blending and without color filter (readback uses Src blending). + SkSamplingOptions sampling(SkFilterMode::kNearest); if (layer->getForceFilter() || shouldFilterRect(totalMatrix, skiaSrcRect, skiaDestRect)) { - paint.setFilterQuality(kLow_SkFilterQuality); + sampling = SkSamplingOptions(SkFilterMode::kLinear); } - canvas->drawImageRect(layerImage.get(), skiaSrcRect, skiaDestRect, &paint, + canvas->drawImageRect(layerImage.get(), skiaSrcRect, skiaDestRect, sampling, &paint, SkCanvas::kFast_SrcRectConstraint); } else { SkRect imageRect = SkRect::MakeIWH(layerImage->width(), layerImage->height()); + SkSamplingOptions sampling(SkFilterMode::kNearest); if (layer->getForceFilter() || shouldFilterRect(totalMatrix, imageRect, imageRect)) { - paint.setFilterQuality(kLow_SkFilterQuality); + sampling = SkSamplingOptions(SkFilterMode::kLinear); } - canvas->drawImage(layerImage.get(), 0, 0, &paint); + canvas->drawImage(layerImage.get(), 0, 0, sampling, &paint); } // restore the original matrix if (nonIdentityMatrix) { 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..c01021221f37 100644 --- a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp +++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp @@ -20,6 +20,8 @@ #include "SkiaDisplayList.h" #include "utils/TraceUtils.h" +#include <include/effects/SkImageFilters.h> + #include <optional> namespace android { @@ -61,12 +63,11 @@ void RenderNodeDrawable::drawBackwardsProjectedNodes(SkCanvas* canvas, SkAutoCanvasRestore acr(canvas, true); SkMatrix nodeMatrix; mat4 hwuiMatrix(child.getRecordedMatrix()); - auto childNode = child.getRenderNode(); + const RenderNode* childNode = child.getRenderNode(); childNode->applyViewPropertyTransforms(hwuiMatrix); hwuiMatrix.copyTo(nodeMatrix); canvas->concat(nodeMatrix); - SkiaDisplayList* childDisplayList = static_cast<SkiaDisplayList*>( - (const_cast<DisplayList*>(childNode->getDisplayList()))); + const SkiaDisplayList* childDisplayList = childNode->getDisplayList().asSkiaDl(); if (childDisplayList) { drawBackwardsProjectedNodes(canvas, *childDisplayList, nestLevel + 1); } @@ -144,7 +145,7 @@ void RenderNodeDrawable::forceDraw(SkCanvas* canvas) const { return; } - SkiaDisplayList* displayList = (SkiaDisplayList*)renderNode->getDisplayList(); + SkiaDisplayList* displayList = renderNode->getDisplayList().asSkiaDl(); SkAutoCanvasRestore acr(canvas, true); const RenderProperties& properties = this->getNodeProperties(); @@ -170,12 +171,27 @@ void RenderNodeDrawable::forceDraw(SkCanvas* canvas) const { static bool layerNeedsPaint(const LayerProperties& properties, float alphaMultiplier, 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 || !properties.getStretchEffect().isEmpty()) { paint->setAlpha(properties.alpha() * alphaMultiplier); paint->setBlendMode(properties.xferMode()); paint->setColorFilter(sk_ref_sp(properties.getColorFilter())); + + sk_sp<SkImageFilter> imageFilter = sk_ref_sp(properties.getImageFilter()); + sk_sp<SkImageFilter> stretchFilter = properties.getStretchEffect().getImageFilter(); + sk_sp<SkImageFilter> filter; + if (imageFilter && stretchFilter) { + filter = SkImageFilters::Compose( + std::move(stretchFilter), + std::move(imageFilter) + ); + } else if (stretchFilter) { + filter = std::move(stretchFilter); + } else { + filter = std::move(imageFilter); + } + paint->setImageFilter(std::move(filter)); return true; } return false; @@ -211,20 +227,21 @@ void RenderNodeDrawable::drawContent(SkCanvas* canvas) const { if (mComposeLayer) { setViewProperties(properties, canvas, &alphaMultiplier); } - SkiaDisplayList* displayList = (SkiaDisplayList*)mRenderNode->getDisplayList(); + SkiaDisplayList* displayList = mRenderNode->getDisplayList().asSkiaDl(); displayList->mParentMatrix = canvas->getTotalMatrix(); // TODO should we let the bound of the drawable do this for us? const SkRect bounds = SkRect::MakeWH(properties.getWidth(), properties.getHeight()); bool quickRejected = properties.getClipToBounds() && canvas->quickReject(bounds); if (!quickRejected) { - SkiaDisplayList* displayList = (SkiaDisplayList*)renderNode->getDisplayList(); + SkiaDisplayList* displayList = renderNode->getDisplayList().asSkiaDl(); const LayerProperties& layerProperties = properties.layerProperties(); // composing a hardware layer if (renderNode->getLayerSurface() && mComposeLayer) { SkASSERT(properties.effectiveLayerType() == LayerType::RenderLayer); SkPaint paint; layerNeedsPaint(layerProperties, alphaMultiplier, &paint); + SkSamplingOptions sampling(SkFilterMode::kLinear); // surfaces for layers are created on LAYER_SIZE boundaries (which are >= layer size) so // we need to restrict the portion of the surface drawn to the size of the renderNode. @@ -238,7 +255,7 @@ void RenderNodeDrawable::drawContent(SkCanvas* canvas) const { "SurfaceID|%" PRId64, renderNode->uniqueId()).c_str(), nullptr); } canvas->drawImageRect(renderNode->getLayerSurface()->makeImageSnapshot(), bounds, - bounds, &paint); + bounds, sampling, &paint, SkCanvas::kStrict_SrcRectConstraint); if (!renderNode->getSkiaLayer()->hasRenderedSinceRepaint) { renderNode->getSkiaLayer()->hasRenderedSinceRepaint = true; 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/SkiaDisplayList.cpp b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp index 2edd48c5a41b..3498f715b455 100644 --- a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp +++ b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp @@ -47,7 +47,7 @@ void SkiaDisplayList::syncContents(const WebViewSyncData& data) { } } -bool SkiaDisplayList::reuseDisplayList(RenderNode* node, renderthread::CanvasContext* context) { +bool SkiaDisplayList::reuseDisplayList(RenderNode* node) { reset(); node->attachAvailableList(this); return true; @@ -172,7 +172,7 @@ void SkiaDisplayList::reset() { new (&allocator) LinearAllocator(); } -void SkiaDisplayList::output(std::ostream& output, uint32_t level) { +void SkiaDisplayList::output(std::ostream& output, uint32_t level) const { DumpOpsCanvas canvas(output, level, *this); mDisplayList.draw(&canvas); } diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.h b/libs/hwui/pipeline/skia/SkiaDisplayList.h index cdd00db9afdc..483264f95e60 100644 --- a/libs/hwui/pipeline/skia/SkiaDisplayList.h +++ b/libs/hwui/pipeline/skia/SkiaDisplayList.h @@ -98,7 +98,7 @@ public: * * @return true if the displayList will be reused and therefore should not be deleted */ - bool reuseDisplayList(RenderNode* node, renderthread::CanvasContext* context); + bool reuseDisplayList(RenderNode* node); /** * ONLY to be called by RenderNode::syncDisplayList so that we can notify any @@ -142,7 +142,7 @@ public: void draw(SkCanvas* canvas) { mDisplayList.draw(canvas); } - void output(std::ostream& output, uint32_t level); + void output(std::ostream& output, uint32_t level) const; LinearAllocator allocator; 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..6456e36a847a 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++) { @@ -97,7 +98,7 @@ void SkiaPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) continue; } SkASSERT(layerNode->getLayerSurface()); - SkiaDisplayList* displayList = (SkiaDisplayList*)layerNode->getDisplayList(); + SkiaDisplayList* displayList = layerNode->getDisplayList().asSkiaDl(); if (!displayList || displayList->isEmpty()) { ALOGE("%p drawLayers(%s) : missing drawable", layerNode, layerNode->getName()); return; @@ -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(); @@ -274,7 +276,10 @@ bool SkiaPipeline::setupMultiFrameCapture() { // we need to keep it until after mMultiPic.close() // procs is passed as a pointer, but just as a method of having an optional default. // procs doesn't need to outlive this Make call. - mMultiPic = SkMakeMultiPictureDocument(mOpenMultiPicStream.get(), &procs); + mMultiPic = SkMakeMultiPictureDocument(mOpenMultiPicStream.get(), &procs, + [sharingCtx = mSerialContext.get()](const SkPicture* pic) { + SkSharingSerialContext::collectNonTextureImagesFromPicture(pic, sharingCtx); + }); return true; } else { ALOGE("Could not open \"%s\" for writing.", mCapturedFile.c_str()); @@ -286,7 +291,7 @@ bool SkiaPipeline::setupMultiFrameCapture() { // recurse through the rendernode's children, add any nodes which are layers to the queue. static void collectLayers(RenderNode* node, LayerUpdateQueue* layers) { - SkiaDisplayList* dl = (SkiaDisplayList*)node->getDisplayList(); + SkiaDisplayList* dl = node->getDisplayList().asSkiaDl(); if (dl) { const auto& prop = node->properties(); if (node->hasLayer()) { @@ -450,7 +455,7 @@ void SkiaPipeline::renderFrame(const LayerUpdateQueue& layers, const SkRect& cli } ATRACE_NAME("flush commands"); - surface->getCanvas()->flush(); + surface->flushAndSubmit(); Properties::skpCaptureEnabled = previousSkpEnabled; } @@ -587,14 +592,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; } } @@ -642,7 +656,7 @@ void SkiaPipeline::renderOverdraw(const SkRect& clip, SkPaint paint; const SkColor* colors = kOverdrawColors[static_cast<int>(Properties::overdrawColorSet)]; paint.setColorFilter(SkOverdrawColorFilter::MakeWithSkColors(colors)); - surface->getCanvas()->drawImage(counts.get(), 0.0f, 0.0f, &paint); + surface->getCanvas()->drawImage(counts.get(), 0.0f, 0.0f, SkSamplingOptions(), &paint); } } /* namespace skiapipeline */ 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 621f2863fa03..ee7c4d8bb54a 100644 --- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp +++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp @@ -55,11 +55,15 @@ void SkiaRecordingCanvas::initDisplayList(uirenderer::RenderNode* renderNode, in SkiaCanvas::reset(&mRecorder); } -uirenderer::DisplayList* SkiaRecordingCanvas::finishRecording() { +std::unique_ptr<SkiaDisplayList> SkiaRecordingCanvas::finishRecording() { // close any existing chunks if necessary - insertReorderBarrier(false); + enableZ(false); mRecorder.restoreToCount(1); - return mDisplayList.release(); + return std::move(mDisplayList); +} + +void SkiaRecordingCanvas::finishRecording(uirenderer::RenderNode* destination) { + destination->setStagingDisplayList(uirenderer::DisplayList(finishRecording())); } // ---------------------------------------------------------------------------- @@ -85,8 +89,18 @@ void SkiaRecordingCanvas::drawCircle(uirenderer::CanvasPropertyPrimitive* x, drawDrawable(mDisplayList->allocateDrawable<AnimatedCircle>(x, y, radius, paint)); } -void SkiaRecordingCanvas::insertReorderBarrier(bool enableReorder) { - if (mCurrentBarrier && enableReorder) { +void SkiaRecordingCanvas::drawRipple(uirenderer::CanvasPropertyPrimitive* x, + uirenderer::CanvasPropertyPrimitive* y, + uirenderer::CanvasPropertyPrimitive* radius, + uirenderer::CanvasPropertyPaint* paint, + uirenderer::CanvasPropertyPrimitive* progress, + const SkRuntimeShaderBuilder& effectBuilder) { + drawDrawable(mDisplayList->allocateDrawable<AnimatedRipple>(x, y, radius, paint, progress, + effectBuilder)); +} + +void SkiaRecordingCanvas::enableZ(bool enableZ) { + if (mCurrentBarrier && enableZ) { // Already in a re-order section, nothing to do return; } @@ -98,7 +112,7 @@ void SkiaRecordingCanvas::insertReorderBarrier(bool enableReorder) { mCurrentBarrier = nullptr; drawDrawable(drawable); } - if (enableReorder) { + if (enableZ) { mCurrentBarrier = mDisplayList->allocateDrawable<StartReorderBarrierDrawable>(mDisplayList.get()); drawDrawable(mCurrentBarrier); @@ -132,23 +146,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; @@ -202,18 +199,21 @@ static SkDrawLooper* get_looper(const Paint* paint) { } template <typename Proc> -void applyLooper(SkDrawLooper* looper, const SkPaint& paint, Proc proc) { +void applyLooper(SkDrawLooper* looper, const SkPaint* paint, Proc proc) { if (looper) { SkSTArenaAlloc<256> alloc; SkDrawLooper::Context* ctx = looper->makeContext(&alloc); if (ctx) { SkDrawLooper::Context::Info info; for (;;) { - SkPaint p = paint; + SkPaint p; + if (paint) { + p = *paint; + } if (!ctx->next(&info, &p)) { break; } - proc(info.fTranslate.fX, info.fTranslate.fY, p); + proc(info.fTranslate.fX, info.fTranslate.fY, &p); } } } else { @@ -221,11 +221,22 @@ void applyLooper(SkDrawLooper* looper, const SkPaint& paint, Proc proc) { } } +static SkFilterMode Paint_to_filter(const SkPaint* paint) { + return paint && paint->getFilterQuality() != kNone_SkFilterQuality ? SkFilterMode::kLinear + : SkFilterMode::kNearest; +} + +static SkSamplingOptions Paint_to_sampling(const SkPaint* paint) { + // Android only has 1-bit for "filter", so we don't try to cons-up mipmaps or cubics + return SkSamplingOptions(Paint_to_filter(paint), SkMipmapMode::kNone); +} + void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, float left, float top, const Paint* paint) { sk_sp<SkImage> image = bitmap.makeImage(); - applyLooper(get_looper(paint), *filterBitmap(paint), [&](SkScalar x, SkScalar y, const SkPaint& p) { - mRecorder.drawImage(image, left + x, top + y, &p, bitmap.palette()); + applyLooper(get_looper(paint), filterBitmap(paint), [&](SkScalar x, SkScalar y, + const SkPaint* p) { + mRecorder.drawImage(image, left + x, top + y, Paint_to_sampling(p), p, bitmap.palette()); }); // if image->unique() is true, then mRecorder.drawImage failed for some reason. It also means @@ -242,8 +253,9 @@ void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, const SkMatrix& matrix, con sk_sp<SkImage> image = bitmap.makeImage(); - applyLooper(get_looper(paint), *filterBitmap(paint), [&](SkScalar x, SkScalar y, const SkPaint& p) { - mRecorder.drawImage(image, x, y, &p, bitmap.palette()); + applyLooper(get_looper(paint), filterBitmap(paint), [&](SkScalar x, SkScalar y, + const SkPaint* p) { + mRecorder.drawImage(image, x, y, Paint_to_sampling(p), p, bitmap.palette()); }); if (!bitmap.isImmutable() && image.get() && !image->unique()) { @@ -259,9 +271,10 @@ void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, float srcLeft, float srcTop sk_sp<SkImage> image = bitmap.makeImage(); - applyLooper(get_looper(paint), *filterBitmap(paint), [&](SkScalar x, SkScalar y, const SkPaint& p) { - mRecorder.drawImageRect(image, srcRect, dstRect.makeOffset(x, y), &p, - SkCanvas::kFast_SrcRectConstraint, bitmap.palette()); + applyLooper(get_looper(paint), filterBitmap(paint), [&](SkScalar x, SkScalar y, + const SkPaint* p) { + mRecorder.drawImageRect(image, srcRect, dstRect.makeOffset(x, y), Paint_to_sampling(p), + p, SkCanvas::kFast_SrcRectConstraint, bitmap.palette()); }); if (!bitmap.isImmutable() && image.get() && !image->unique() && !srcRect.isEmpty() && @@ -293,17 +306,15 @@ void SkiaRecordingCanvas::drawNinePatch(Bitmap& bitmap, const Res_png_9patch& ch lattice.fBounds = nullptr; SkRect dst = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom); - - PaintCoW filteredPaint(paint); - // HWUI always draws 9-patches with bilinear filtering, regardless of what is set in the Paint. - if (!filteredPaint || filteredPaint->getFilterQuality() != kLow_SkFilterQuality) { - filteredPaint.writeable().setFilterQuality(kLow_SkFilterQuality); - } sk_sp<SkImage> image = bitmap.makeImage(); - applyLooper(get_looper(paint), *filterBitmap(std::move(filteredPaint)), - [&](SkScalar x, SkScalar y, const SkPaint& p) { - mRecorder.drawImageLattice(image, lattice, dst.makeOffset(x, y), &p, bitmap.palette()); + // HWUI always draws 9-patches with linear filtering, regardless of the Paint. + const SkFilterMode filter = SkFilterMode::kLinear; + + applyLooper(get_looper(paint), filterBitmap(paint), [&](SkScalar x, SkScalar y, + const SkPaint* p) { + mRecorder.drawImageLattice(image, lattice, dst.makeOffset(x, y), filter, p, + bitmap.palette()); }); if (!bitmap.isImmutable() && image.get() && !image->unique() && !dst.isEmpty()) { diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h index bd5274c94e75..8d7a21a732dd 100644 --- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h +++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h @@ -39,11 +39,12 @@ public: } virtual void resetRecording(int width, int height, - uirenderer::RenderNode* renderNode) override { + uirenderer::RenderNode* renderNode = nullptr) override { initDisplayList(renderNode, width, height); } - virtual uirenderer::DisplayList* finishRecording() override; + virtual void finishRecording(uirenderer::RenderNode* destination) override; + std::unique_ptr<SkiaDisplayList> finishRecording(); virtual void drawBitmap(Bitmap& bitmap, float left, float top, const Paint* paint) override; virtual void drawBitmap(Bitmap& bitmap, const SkMatrix& matrix, const Paint* paint) override; @@ -66,14 +67,19 @@ public: uirenderer::CanvasPropertyPrimitive* y, uirenderer::CanvasPropertyPrimitive* radius, uirenderer::CanvasPropertyPaint* paint) override; + virtual void drawRipple(uirenderer::CanvasPropertyPrimitive* x, + uirenderer::CanvasPropertyPrimitive* y, + uirenderer::CanvasPropertyPrimitive* radius, + uirenderer::CanvasPropertyPaint* paint, + uirenderer::CanvasPropertyPrimitive* progress, + const SkRuntimeShaderBuilder& effectBuilder) override; 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..bae11f7d074c 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); - canvas->drawImage(functorImage, 0, 0, &paint); + auto functorImage = SkImage::MakeFromAHardwareBuffer(mFrameBuffer.get(), kPremul_SkAlphaType, + canvas->imageInfo().refColorSpace(), + kBottomLeft_GrSurfaceOrigin); + canvas->drawImage(functorImage, 0, 0, SkSamplingOptions(), &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/private/hwui/WebViewFunctor.h b/libs/hwui/private/hwui/WebViewFunctor.h index 96da947ace08..22ae59e5137b 100644 --- a/libs/hwui/private/hwui/WebViewFunctor.h +++ b/libs/hwui/private/hwui/WebViewFunctor.h @@ -17,6 +17,15 @@ #ifndef FRAMEWORKS_BASE_WEBVIEWFUNCTOR_H #define FRAMEWORKS_BASE_WEBVIEWFUNCTOR_H +#ifdef __ANDROID__ // Layoutlib does not support surface control +#include <android/surface_control.h> +#else +// To avoid ifdefs around overlay implementation all over the place we typedef these to void *. They +// won't be used. +typedef void* ASurfaceControl; +typedef void* ASurfaceTransaction; +#endif + #include <cutils/compiler.h> #include <private/hwui/DrawGlInfo.h> #include <private/hwui/DrawVkInfo.h> @@ -28,6 +37,14 @@ enum class RenderMode { Vulkan, }; +enum class OverlaysMode { + // Indicated that webview should not promote anything to overlays this draw + // and remove all visible overlays. + Disabled, + // Indicates that webview can use overlays. + Enabled +}; + // Static for the lifetime of the process ANDROID_API RenderMode WebViewFunctor_queryPlatformRenderMode(); @@ -35,6 +52,23 @@ struct WebViewSyncData { bool applyForceDark; }; +struct WebViewOverlayData { + // Desired overlay mode for this draw. + OverlaysMode overlaysMode; + + // Returns parent ASurfaceControl for WebView overlays. It will be have same + // geometry as the surface we draw into and positioned below it (underlay). + // This does not pass ownership to webview, but guaranteed to be alive until + // transaction from next removeOverlays call or functor destruction will be + // finished. + ASurfaceControl* (*getSurfaceControl)(); + + // Merges WebView transaction to be applied synchronously with current draw. + // This doesn't pass ownership of the transaction, changes will be copied and + // webview can free transaction right after the call. + void (*mergeTransaction)(ASurfaceTransaction*); +}; + struct WebViewFunctorCallbacks { // kModeSync, called on RenderThread void (*onSync)(int functor, void* data, const WebViewSyncData& syncData); @@ -48,16 +82,23 @@ struct WebViewFunctorCallbacks { // this functor had ever been drawn. void (*onDestroyed)(int functor, void* data); + // Called on render thread to force webview hide all overlays and stop updating them. + // Should happen during hwui draw (e.g can be called instead of draw if webview + // isn't visible and won't receive draw) and support MergeTransaction call. + void (*removeOverlays)(int functor, void* data, void (*mergeTransaction)(ASurfaceTransaction*)); + union { struct { // Called on RenderThread. initialize is guaranteed to happen before this call - void (*draw)(int functor, void* data, const DrawGlInfo& params); + void (*draw)(int functor, void* data, const DrawGlInfo& params, + const WebViewOverlayData& overlayParams); } gles; struct { // Called either the first time the functor is used or the first time it's used after // a call to onContextDestroyed. void (*initialize)(int functor, void* data, const VkFunctorInitParams& params); - void (*draw)(int functor, void* data, const VkFunctorDrawParams& params); + void (*draw)(int functor, void* data, const VkFunctorDrawParams& params, + const WebViewOverlayData& overlayParams); void (*postDraw)(int functor, void*); } vk; }; 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..37a6ee71c4a6 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -157,12 +157,14 @@ static void setBufferCount(ANativeWindow* window) { void CanvasContext::setSurface(ANativeWindow* window, bool enableTimeout) { ATRACE_CALL(); - if (mRenderAheadDepth == 0 && DeviceInfo::get()->getMaxRefreshRate() > 66.6f) { - mFixedRenderAhead = false; - mRenderAheadCapacity = 1; - } else { - mFixedRenderAhead = true; + if (mFixedRenderAhead) { mRenderAheadCapacity = mRenderAheadDepth; + } else { + if (DeviceInfo::get()->getMaxRefreshRate() > 66.6f) { + mRenderAheadCapacity = 1; + } else { + mRenderAheadCapacity = 0; + } } if (window) { @@ -176,7 +178,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 +192,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 +247,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 +467,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 +490,16 @@ void CanvasContext::draw() { waitOnFences(); + if (mNativeSurface) { + // TODO(b/165985262): measure performance impact + const auto vsyncId = mCurrentFrameInfo->get(FrameInfoIndex::FrameTimelineVsyncId); + if (vsyncId != UiFrameInfoBuilder::INVALID_VSYNC_ID) { + const auto inputEventId = mCurrentFrameInfo->get(FrameInfoIndex::NewestInputEvent); + native_window_set_frame_timeline_info(mNativeSurface->getNativeWindow(), vsyncId, + inputEventId); + } + } + bool requireSwap = false; int error = OK; bool didSwap = @@ -617,8 +633,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); @@ -746,11 +766,16 @@ bool CanvasContext::surfaceRequiresRedraw() { } void CanvasContext::setRenderAheadDepth(int renderAhead) { - if (renderAhead > 2 || renderAhead < 0 || mNativeSurface) { + if (renderAhead > 2 || renderAhead < -1 || mNativeSurface) { return; } - mFixedRenderAhead = true; - mRenderAheadDepth = static_cast<uint32_t>(renderAhead); + if (renderAhead == -1) { + mFixedRenderAhead = false; + mRenderAheadDepth = 0; + } else { + mFixedRenderAhead = true; + mRenderAheadDepth = static_cast<uint32_t>(renderAhead); + } } SkRect CanvasContext::computeDirtyRect(const Frame& frame, SkRect* dirty) { 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..7750a31b817f 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,17 @@ 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 + // Also check if we have a GrContext before returning fast. VulkanManager may be shared with + // the HardwareBitmapUploader which initializes the Vk context without persisting the GrContext + // in the rendering thread. + if (vulkanManager().hasVkContext() && mGrContext) { return; } mVkManager->initialize(); @@ -204,7 +210,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 +228,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 +274,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..3e7ce368f55d 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -20,13 +20,15 @@ #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> #include <vk/GrVkExtensions.h> #include <vk/GrVkTypes.h> +#include <cstring> + #include "Properties.h" #include "RenderThread.h" #include "renderstate/RenderState.h" @@ -53,16 +55,39 @@ static void free_features_extensions_structs(const VkPhysicalDeviceFeatures2& fe } } +GrVkGetProc VulkanManager::sSkiaGetProp = [](const char* proc_name, VkInstance instance, + VkDevice device) { + if (device != VK_NULL_HANDLE) { + if (strcmp("vkQueueSubmit", proc_name) == 0) { + return (PFN_vkVoidFunction)VulkanManager::interceptedVkQueueSubmit; + } else if (strcmp("vkQueueWaitIdle", proc_name) == 0) { + return (PFN_vkVoidFunction)VulkanManager::interceptedVkQueueWaitIdle; + } + return vkGetDeviceProcAddr(device, proc_name); + } + return vkGetInstanceProcAddr(instance, proc_name); +}; + #define GET_PROC(F) m##F = (PFN_vk##F)vkGetInstanceProcAddr(VK_NULL_HANDLE, "vk" #F) #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 +98,6 @@ void VulkanManager::destroy() { } mGraphicsQueue = VK_NULL_HANDLE; - mPresentQueue = VK_NULL_HANDLE; mDevice = VK_NULL_HANDLE; mPhysicalDevice = VK_NULL_HANDLE; mInstance = VK_NULL_HANDLE; @@ -180,10 +204,6 @@ void VulkanManager::setupDevice(GrVkExtensions& grExtensions, VkPhysicalDeviceFe } LOG_ALWAYS_FATAL_IF(mGraphicsQueueIndex == queueCount); - // All physical devices and queue families on Android must be capable of - // presentation with any native window. So just use the first one. - mPresentQueueIndex = 0; - { uint32_t extensionCount = 0; err = mEnumerateDeviceExtensionProperties(mPhysicalDevice, nullptr, &extensionCount, @@ -203,14 +223,7 @@ void VulkanManager::setupDevice(GrVkExtensions& grExtensions, VkPhysicalDeviceFe LOG_ALWAYS_FATAL_IF(!hasKHRSwapchainExtension); } - auto getProc = [](const char* proc_name, VkInstance instance, VkDevice device) { - if (device != VK_NULL_HANDLE) { - return vkGetDeviceProcAddr(device, proc_name); - } - return vkGetInstanceProcAddr(instance, proc_name); - }; - - grExtensions.init(getProc, mInstance, mPhysicalDevice, mInstanceExtensions.size(), + grExtensions.init(sSkiaGetProp, mInstance, mPhysicalDevice, mInstanceExtensions.size(), mInstanceExtensions.data(), mDeviceExtensions.size(), mDeviceExtensions.data()); @@ -277,31 +290,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 + 1, // 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 @@ -348,34 +351,13 @@ void VulkanManager::initialize() { 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); - if (Properties::enablePartialUpdates && Properties::useBufferAge) { mSwapBehavior = SwapBehavior::BufferAge; } } -sk_sp<GrContext> VulkanManager::createContext(const GrContextOptions& options) { - auto getProc = [](const char* proc_name, VkInstance instance, VkDevice device) { - if (device != VK_NULL_HANDLE) { - return vkGetDeviceProcAddr(device, proc_name); - } - return vkGetInstanceProcAddr(instance, proc_name); - }; +sk_sp<GrDirectContext> VulkanManager::createContext(const GrContextOptions& options, + ContextType contextType) { GrVkBackendContext backendContext; backendContext.fInstance = mInstance; @@ -386,9 +368,9 @@ sk_sp<GrContext> VulkanManager::createContext(const GrContextOptions& options) { backendContext.fMaxAPIVersion = mAPIVersion; backendContext.fVkExtensions = &mExtensions; backendContext.fDeviceFeatures2 = &mPhysicalDeviceFeatures2; - backendContext.fGetProc = std::move(getProc); + backendContext.fGetProc = sSkiaGetProp; - return GrContext::MakeVulkan(backendContext, options); + return GrDirectContext::MakeVulkan(backendContext, options); } VkFunctorInitParams VulkanManager::getVkFunctorInitParams() const { @@ -459,7 +441,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 +507,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; @@ -539,6 +528,8 @@ void VulkanManager::swapBuffers(VulkanSurface* surface, const SkRect& dirtyRect) ALOGE_IF(VK_SUCCESS != err, "VulkanManager::swapBuffers(): Failed to get semaphore Fd"); } else { ALOGE("VulkanManager::swapBuffers(): Semaphore submission failed"); + + std::lock_guard<std::mutex> lock(mGraphicsQueueMutex); mQueueWaitIdle(mGraphicsQueue); } destroy_semaphore(destroyInfo); @@ -548,17 +539,20 @@ 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) { + std::lock_guard<std::mutex> lock(mGraphicsQueueMutex); + 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 +563,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 +606,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 +642,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..121afc90cfe5 100644 --- a/libs/hwui/renderthread/VulkanManager.h +++ b/libs/hwui/renderthread/VulkanManager.h @@ -17,6 +17,10 @@ #ifndef VULKANMANAGER_H #define VULKANMANAGER_H +#include <functional> +#include <mutex> + +#include "vulkan/vulkan_core.h" #if !defined(VK_USE_PLATFORM_ANDROID_KHR) #define VK_USE_PLATFORM_ANDROID_KHR #endif @@ -43,10 +47,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 +60,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&); @@ -151,10 +165,25 @@ private: VkDevice mDevice = VK_NULL_HANDLE; uint32_t mGraphicsQueueIndex; + + std::mutex mGraphicsQueueMutex; VkQueue mGraphicsQueue = VK_NULL_HANDLE; - uint32_t mPresentQueueIndex; - VkQueue mPresentQueue = VK_NULL_HANDLE; - VkCommandPool mCommandPool = VK_NULL_HANDLE; + + static VKAPI_ATTR VkResult interceptedVkQueueSubmit(VkQueue queue, uint32_t submitCount, + const VkSubmitInfo* pSubmits, + VkFence fence) { + sp<VulkanManager> manager = VulkanManager::getInstance(); + std::lock_guard<std::mutex> lock(manager->mGraphicsQueueMutex); + return manager->mQueueSubmit(queue, submitCount, pSubmits, fence); + } + + static VKAPI_ATTR VkResult interceptedVkQueueWaitIdle(VkQueue queue) { + sp<VulkanManager> manager = VulkanManager::getInstance(); + std::lock_guard<std::mutex> lock(manager->mGraphicsQueueMutex); + return manager->mQueueWaitIdle(queue); + } + + static GrVkGetProc sSkiaGetProp; // 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/CallCountingCanvas.h b/libs/hwui/tests/common/CallCountingCanvas.h new file mode 100644 index 000000000000..d3c41191eef1 --- /dev/null +++ b/libs/hwui/tests/common/CallCountingCanvas.h @@ -0,0 +1,168 @@ +/* + * 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 <SkCanvasVirtualEnforcer.h> +#include <SkNoDrawCanvas.h> + +namespace android { +namespace uirenderer { +namespace test { + +class CallCountingCanvas final : public SkCanvasVirtualEnforcer<SkNoDrawCanvas> { +private: + int START_MARKER; +public: + CallCountingCanvas() : SkCanvasVirtualEnforcer<SkNoDrawCanvas>(1, 1) {} + + int sumTotalDrawCalls() { + // Dirty hack assumes we're nothing but ints between START_MARKET and END_MARKER + int* cur = &START_MARKER + 1; + int* end = &END_MARKER; + int sum = 0; + while (cur != end) { + sum += *cur; + cur++; + } + return sum; + } + + int drawPaintCount = 0; + void onDrawPaint(const SkPaint& paint) override { + drawPaintCount++; + } + + int drawBehindCount = 0; + void onDrawBehind(const SkPaint&) override { + drawBehindCount++; + } + + int drawRectCount = 0; + void onDrawRect(const SkRect& rect, const SkPaint& paint) override { + drawRectCount++; + } + + int drawRRectCount = 0; + void onDrawRRect(const SkRRect& rrect, const SkPaint& paint) override { + drawRRectCount++; + } + + int drawDRRectCount = 0; + void onDrawDRRect(const SkRRect& outer, const SkRRect& inner, + const SkPaint& paint) override { + drawDRRectCount++; + } + + int drawOvalCount = 0; + void onDrawOval(const SkRect& rect, const SkPaint& paint) override { + drawOvalCount++; + } + + int drawArcCount = 0; + void onDrawArc(const SkRect& rect, SkScalar startAngle, SkScalar sweepAngle, bool useCenter, + const SkPaint& paint) override { + drawArcCount++; + } + + int drawPathCount = 0; + void onDrawPath(const SkPath& path, const SkPaint& paint) override { + drawPathCount++; + } + + int drawRegionCount = 0; + void onDrawRegion(const SkRegion& region, const SkPaint& paint) override { + drawRegionCount++; + } + + int drawTextBlobCount = 0; + void onDrawTextBlob(const SkTextBlob* blob, SkScalar x, SkScalar y, + const SkPaint& paint) override { + drawTextBlobCount++; + } + + int drawPatchCount = 0; + void onDrawPatch(const SkPoint cubics[12], const SkColor colors[4], + const SkPoint texCoords[4], SkBlendMode mode, + const SkPaint& paint) override { + drawPatchCount++; + } + + int drawPoints = 0; + void onDrawPoints(SkCanvas::PointMode mode, size_t count, const SkPoint pts[], + const SkPaint& paint) override { + drawPoints++; + } + + int drawImageCount = 0; + void onDrawImage2(const SkImage* image, SkScalar dx, SkScalar dy, const SkSamplingOptions&, + const SkPaint* paint) override { + drawImageCount++; + } + + int drawImageRectCount = 0; + void onDrawImageRect2(const SkImage*, const SkRect&, const SkRect&, const SkSamplingOptions&, + const SkPaint*, SkCanvas::SrcRectConstraint) override { + drawImageRectCount++; + } + + int drawImageLatticeCount = 0; + void onDrawImageLattice2(const SkImage* image, const SkCanvas::Lattice& lattice, + const SkRect& dst, SkFilterMode, const SkPaint* paint) override { + drawImageLatticeCount++; + } + + int drawAtlasCount = 0; + void onDrawAtlas2(const SkImage* atlas, const SkRSXform xform[], const SkRect rect[], + const SkColor colors[], int count, SkBlendMode mode, const SkSamplingOptions&, + const SkRect* cull, const SkPaint* paint) override { + drawAtlasCount++; + } + + int drawAnnotationCount = 0; + void onDrawAnnotation(const SkRect& rect, const char key[], SkData* value) override { + drawAnnotationCount++; + } + + int drawShadowRecCount = 0; + void onDrawShadowRec(const SkPath&, const SkDrawShadowRec&) override { + drawShadowRecCount++; + } + + int drawDrawableCount = 0; + void onDrawDrawable(SkDrawable* drawable, const SkMatrix* matrix) override { + drawDrawableCount++; + } + + int drawPictureCount = 0; + void onDrawPicture(const SkPicture* picture, const SkMatrix* matrix, + const SkPaint* paint) override { + drawPictureCount++; + } + + int drawVerticesCount = 0; + void onDrawVerticesObject (const SkVertices *vertices, SkBlendMode mode, + const SkPaint &paint) override { + drawVerticesCount++; + } + +private: + int END_MARKER; +}; + +} /* namespace test */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/tests/common/TestListViewSceneBase.cpp b/libs/hwui/tests/common/TestListViewSceneBase.cpp index fd331333d38a..43df4a0b1576 100644 --- a/libs/hwui/tests/common/TestListViewSceneBase.cpp +++ b/libs/hwui/tests/common/TestListViewSceneBase.cpp @@ -70,7 +70,7 @@ void TestListViewSceneBase::doFrame(int frameNr) { // draw it to parent DisplayList canvas->drawRenderNode(mListItems[ci].get()); } - mListView->setStagingDisplayList(canvas->finishRecording()); + canvas->finishRecording(mListView.get()); } } // namespace test diff --git a/libs/hwui/tests/common/TestUtils.h b/libs/hwui/tests/common/TestUtils.h index 91a808df3657..cf8fc82abb4a 100644 --- a/libs/hwui/tests/common/TestUtils.h +++ b/libs/hwui/tests/common/TestUtils.h @@ -50,12 +50,12 @@ namespace uirenderer { ADD_FAILURE() << "ClipState not a rect"; \ } -#define INNER_PIPELINE_TEST(test_case_name, test_name, pipeline, functionCall) \ - TEST(test_case_name, test_name##_##pipeline) { \ - RenderPipelineType oldType = Properties::getRenderPipelineType(); \ - Properties::overrideRenderPipelineType(RenderPipelineType::pipeline); \ - functionCall; \ - Properties::overrideRenderPipelineType(oldType); \ +#define INNER_PIPELINE_TEST(test_case_name, test_name, pipeline, functionCall) \ + TEST(test_case_name, test_name##_##pipeline) { \ + RenderPipelineType oldType = Properties::getRenderPipelineType(); \ + Properties::overrideRenderPipelineType(RenderPipelineType::pipeline, true); \ + functionCall; \ + Properties::overrideRenderPipelineType(oldType, true); \ }; #define INNER_PIPELINE_RENDERTHREAD_TEST(test_case_name, test_name, pipeline) \ @@ -67,29 +67,27 @@ namespace uirenderer { * Like gtest's TEST, but runs on the RenderThread, and 'renderThread' is passed, in top level scope * (for e.g. accessing its RenderState) */ -#define RENDERTHREAD_TEST(test_case_name, test_name) \ - class test_case_name##_##test_name##_RenderThreadTest { \ - public: \ - static void doTheThing(renderthread::RenderThread& renderThread); \ - }; \ - INNER_PIPELINE_RENDERTHREAD_TEST(test_case_name, test_name, SkiaGL); \ - /* Temporarily disabling Vulkan until we can figure out a way to stub out the driver */ \ - /* INNER_PIPELINE_RENDERTHREAD_TEST(test_case_name, test_name, SkiaVulkan); */ \ - void test_case_name##_##test_name##_RenderThreadTest::doTheThing( \ +#define RENDERTHREAD_TEST(test_case_name, test_name) \ + class test_case_name##_##test_name##_RenderThreadTest { \ + public: \ + static void doTheThing(renderthread::RenderThread& renderThread); \ + }; \ + INNER_PIPELINE_RENDERTHREAD_TEST(test_case_name, test_name, SkiaGL); \ + INNER_PIPELINE_RENDERTHREAD_TEST(test_case_name, test_name, SkiaVulkan); \ + void test_case_name##_##test_name##_RenderThreadTest::doTheThing( \ renderthread::RenderThread& renderThread) /** * Like RENDERTHREAD_TEST, but only runs with the Skia RenderPipelineTypes */ -#define RENDERTHREAD_SKIA_PIPELINE_TEST(test_case_name, test_name) \ - class test_case_name##_##test_name##_RenderThreadTest { \ - public: \ - static void doTheThing(renderthread::RenderThread& renderThread); \ - }; \ - INNER_PIPELINE_RENDERTHREAD_TEST(test_case_name, test_name, SkiaGL); \ - /* Temporarily disabling Vulkan until we can figure out a way to stub out the driver */ \ - /* INNER_PIPELINE_RENDERTHREAD_TEST(test_case_name, test_name, SkiaVulkan); */ \ - void test_case_name##_##test_name##_RenderThreadTest::doTheThing( \ +#define RENDERTHREAD_SKIA_PIPELINE_TEST(test_case_name, test_name) \ + class test_case_name##_##test_name##_RenderThreadTest { \ + public: \ + static void doTheThing(renderthread::RenderThread& renderThread); \ + }; \ + INNER_PIPELINE_RENDERTHREAD_TEST(test_case_name, test_name, SkiaGL); \ + INNER_PIPELINE_RENDERTHREAD_TEST(test_case_name, test_name, SkiaVulkan); \ + void test_case_name##_##test_name##_RenderThreadTest::doTheThing( \ renderthread::RenderThread& renderThread) /** @@ -161,14 +159,6 @@ public: renderthread::RenderThread& renderThread, uint32_t width, uint32_t height, const SkMatrix& transform); - template <class CanvasType> - static std::unique_ptr<DisplayList> createDisplayList( - int width, int height, std::function<void(CanvasType& canvas)> canvasCallback) { - CanvasType canvas(width, height); - canvasCallback(canvas); - return std::unique_ptr<DisplayList>(canvas.finishRecording()); - } - static sp<RenderNode> createNode( int left, int top, int right, int bottom, std::function<void(RenderProperties& props, Canvas& canvas)> setup) { @@ -179,7 +169,7 @@ public: std::unique_ptr<Canvas> canvas( Canvas::create_recording_canvas(props.getWidth(), props.getHeight())); setup(props, *canvas.get()); - node->setStagingDisplayList(canvas->finishRecording()); + canvas->finishRecording(node.get()); } node->setPropertyFieldsDirty(0xFFFFFFFF); return node; @@ -205,14 +195,15 @@ public: std::unique_ptr<Canvas> canvas(Canvas::create_recording_canvas( node.stagingProperties().getWidth(), node.stagingProperties().getHeight(), &node)); contentCallback(*canvas.get()); - node.setStagingDisplayList(canvas->finishRecording()); + canvas->finishRecording(&node); } static sp<RenderNode> createSkiaNode( int left, int top, int right, int bottom, std::function<void(RenderProperties& props, skiapipeline::SkiaRecordingCanvas& canvas)> setup, - const char* name = nullptr, skiapipeline::SkiaDisplayList* displayList = nullptr) { + const char* name = nullptr, + std::unique_ptr<skiapipeline::SkiaDisplayList> displayList = nullptr) { sp<RenderNode> node = new RenderNode(); if (name) { node->setName(name); @@ -220,14 +211,14 @@ public: RenderProperties& props = node->mutateStagingProperties(); props.setLeftTopRightBottom(left, top, right, bottom); if (displayList) { - node->setStagingDisplayList(displayList); + node->setStagingDisplayList(DisplayList(std::move(displayList))); } if (setup) { std::unique_ptr<skiapipeline::SkiaRecordingCanvas> canvas( new skiapipeline::SkiaRecordingCanvas(nullptr, props.getWidth(), props.getHeight())); setup(props, *canvas.get()); - node->setStagingDisplayList(canvas->finishRecording()); + canvas->finishRecording(node.get()); } node->setPropertyFieldsDirty(0xFFFFFFFF); TestUtils::syncHierarchyPropertiesAndDisplayList(node); @@ -287,18 +278,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 +290,33 @@ 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(); + callbacks.gles.draw = [](int functor, void* client_data, const DrawGlInfo& params, + const WebViewOverlayData& overlay_params) { + expectOnRenderThread("draw"); sMockFunctorCounts[functor].glesDraw++; }; break; @@ -357,13 +339,11 @@ private: node->mNeedsDisplayListSync = false; node->syncDisplayList(observer, nullptr); } - auto displayList = node->getDisplayList(); + auto& displayList = node->getDisplayList(); if (displayList) { - for (auto&& childDr : - static_cast<skiapipeline::SkiaDisplayList*>(const_cast<DisplayList*>(displayList)) - ->mChildNodes) { - syncHierarchyPropertiesAndDisplayListImpl(childDr.getRenderNode()); - } + displayList.updateChildren([](RenderNode* child) { + syncHierarchyPropertiesAndDisplayListImpl(child); + }); } } diff --git a/libs/hwui/tests/common/scenes/BitmapShaders.cpp b/libs/hwui/tests/common/scenes/BitmapShaders.cpp index c4067af388e3..03aeb55f129b 100644 --- a/libs/hwui/tests/common/scenes/BitmapShaders.cpp +++ b/libs/hwui/tests/common/scenes/BitmapShaders.cpp @@ -44,15 +44,16 @@ public: skCanvas.drawRect(SkRect::MakeXYWH(100, 100, 100, 100), skPaint); }); + SkSamplingOptions sampling; Paint paint; sk_sp<SkImage> image = hwuiBitmap->makeImage(); sk_sp<SkShader> repeatShader = - image->makeShader(SkTileMode::kRepeat, SkTileMode::kRepeat); + image->makeShader(SkTileMode::kRepeat, SkTileMode::kRepeat, sampling); paint.setShader(std::move(repeatShader)); canvas.drawRoundRect(0, 0, 500, 500, 50.0f, 50.0f, paint); sk_sp<SkShader> mirrorShader = - image->makeShader(SkTileMode::kMirror, SkTileMode::kMirror); + image->makeShader(SkTileMode::kMirror, SkTileMode::kMirror, sampling); paint.setShader(std::move(mirrorShader)); canvas.drawRoundRect(0, 600, 500, 1100, 50.0f, 50.0f, paint); } diff --git a/libs/hwui/tests/common/scenes/GlyphStressAnimation.cpp b/libs/hwui/tests/common/scenes/GlyphStressAnimation.cpp index 0795d13f441b..4271d2f04b88 100644 --- a/libs/hwui/tests/common/scenes/GlyphStressAnimation.cpp +++ b/libs/hwui/tests/common/scenes/GlyphStressAnimation.cpp @@ -55,6 +55,6 @@ public: TestUtils::drawUtf8ToCanvas(canvas.get(), text, paint, 0, 100 * (i + 2)); } - container->setStagingDisplayList(canvas->finishRecording()); + canvas->finishRecording(container.get()); } }; diff --git a/libs/hwui/tests/common/scenes/HwBitmapInCompositeShader.cpp b/libs/hwui/tests/common/scenes/HwBitmapInCompositeShader.cpp index 5886ea39acce..564354f04674 100644 --- a/libs/hwui/tests/common/scenes/HwBitmapInCompositeShader.cpp +++ b/libs/hwui/tests/common/scenes/HwBitmapInCompositeShader.cpp @@ -74,6 +74,6 @@ public: sk_sp<SkShader> createBitmapShader(Bitmap& bitmap) { sk_sp<SkImage> image = bitmap.makeImage(); - return image->makeShader(); + return image->makeShader(SkSamplingOptions()); } }; diff --git a/libs/hwui/tests/common/scenes/ListOfFadedTextAnimation.cpp b/libs/hwui/tests/common/scenes/ListOfFadedTextAnimation.cpp index a9449b62a1f8..5eaf1853233a 100644 --- a/libs/hwui/tests/common/scenes/ListOfFadedTextAnimation.cpp +++ b/libs/hwui/tests/common/scenes/ListOfFadedTextAnimation.cpp @@ -32,7 +32,7 @@ class ListOfFadedTextAnimation : public TestListViewSceneBase { int itemHeight) override { canvas.drawColor(Color::White, SkBlendMode::kSrcOver); int length = dp(100); - canvas.saveLayer(0, 0, length, itemHeight, nullptr, SaveFlags::HasAlphaLayer); + canvas.saveLayer(0, 0, length, itemHeight, nullptr); Paint textPaint; textPaint.getSkFont().setSize(dp(20)); textPaint.setAntiAlias(true); 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/ReadbackFromHardwareBitmap.cpp b/libs/hwui/tests/common/scenes/ReadbackFromHardwareBitmap.cpp index 1d17a021069b..716d3979bdcb 100644 --- a/libs/hwui/tests/common/scenes/ReadbackFromHardwareBitmap.cpp +++ b/libs/hwui/tests/common/scenes/ReadbackFromHardwareBitmap.cpp @@ -51,7 +51,7 @@ public: hardwareBitmap->height(), &canvasBitmap)); SkCanvas skCanvas(canvasBitmap); - skCanvas.drawBitmap(readback, 0, 0); + skCanvas.drawImage(readback.asImage(), 0, 0); canvas.drawBitmap(*heapBitmap, 0, 0, nullptr); canvas.drawBitmap(*hardwareBitmap, 0, 500, nullptr); 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/SaveLayer2Animation.cpp b/libs/hwui/tests/common/scenes/SaveLayer2Animation.cpp index 8630be87c09c..252f539ffca9 100644 --- a/libs/hwui/tests/common/scenes/SaveLayer2Animation.cpp +++ b/libs/hwui/tests/common/scenes/SaveLayer2Animation.cpp @@ -47,8 +47,7 @@ public: // interleave drawText and drawRect with saveLayer ops for (int i = 0; i < regions; i++, top += smallRectHeight) { - canvas.saveLayer(bounds.fLeft, top, bounds.fRight, top + padding, &mBluePaint, - SaveFlags::ClipToLayer | SaveFlags::MatrixClip); + canvas.saveLayer(bounds.fLeft, top, bounds.fRight, top + padding, &mBluePaint); canvas.drawColor(SkColorSetARGB(255, 255, 255, 0), SkBlendMode::kSrcOver); std::string stri = std::to_string(i); std::string offscreen = "offscreen line " + stri; diff --git a/libs/hwui/tests/common/scenes/SaveLayerAnimation.cpp b/libs/hwui/tests/common/scenes/SaveLayerAnimation.cpp index 97bfba34c790..10ba07905c45 100644 --- a/libs/hwui/tests/common/scenes/SaveLayerAnimation.cpp +++ b/libs/hwui/tests/common/scenes/SaveLayerAnimation.cpp @@ -33,10 +33,10 @@ public: card = TestUtils::createNode(0, 0, 400, 800, [](RenderProperties& props, Canvas& canvas) { // nested clipped saveLayers - canvas.saveLayerAlpha(0, 0, 400, 400, 200, SaveFlags::ClipToLayer); + canvas.saveLayerAlpha(0, 0, 400, 400, 200); canvas.drawColor(Color::Green_700, SkBlendMode::kSrcOver); canvas.clipRect(50, 50, 350, 350, SkClipOp::kIntersect); - canvas.saveLayerAlpha(100, 100, 300, 300, 128, SaveFlags::ClipToLayer); + canvas.saveLayerAlpha(100, 100, 300, 300, 128); canvas.drawColor(Color::Blue_500, SkBlendMode::kSrcOver); canvas.restore(); canvas.restore(); @@ -44,12 +44,14 @@ public: // single unclipped saveLayer canvas.save(SaveFlags::MatrixClip); canvas.translate(0, 400); - canvas.saveLayerAlpha(100, 100, 300, 300, 128, SaveFlags::Flags(0)); // unclipped + int unclippedSaveLayer = canvas.saveUnclippedLayer(100, 100, 300, 300); Paint paint; paint.setAntiAlias(true); paint.setColor(Color::Green_700); canvas.drawCircle(200, 200, 200, paint); - canvas.restore(); + SkPaint alphaPaint; + alphaPaint.setAlpha(128); + canvas.restoreUnclippedLayer(unclippedSaveLayer, alphaPaint); canvas.restore(); }); 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..c6219c485b85 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 { @@ -210,7 +210,7 @@ private: overlay->stagingProperties().getHeight(), overlay.get())); canvas->drawColor((curFrame % 150) << 24, SkBlendMode::kSrcOver); - overlay->setStagingDisplayList(canvas->finishRecording()); + canvas->finishRecording(overlay.get()); cardcanvas->drawRenderNode(overlay.get()); } else { // re-recording image node's canvas, animating ColorFilter @@ -223,11 +223,11 @@ private: paint.setColorFilter(filter); sk_sp<Bitmap> bitmap = mCachedBitmaps[ci]; canvas->drawBitmap(*bitmap, 0, 0, &paint); - image->setStagingDisplayList(canvas->finishRecording()); + canvas->finishRecording(image.get()); cardcanvas->drawRenderNode(image.get()); } - card->setStagingDisplayList(cardcanvas->finishRecording()); + cardcanvas->finishRecording(card.get()); } }; 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/CanvasOpBench.cpp b/libs/hwui/tests/microbench/CanvasOpBench.cpp new file mode 100644 index 000000000000..e7ba471ee807 --- /dev/null +++ b/libs/hwui/tests/microbench/CanvasOpBench.cpp @@ -0,0 +1,97 @@ +/* + * 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. + */ + +#include <benchmark/benchmark.h> + +#include "DisplayList.h" +#include "hwui/Paint.h" +#include "canvas/CanvasOpBuffer.h" +#include "canvas/CanvasFrontend.h" +#include "tests/common/TestUtils.h" + +using namespace android; +using namespace android::uirenderer; + +void BM_CanvasOpBuffer_alloc(benchmark::State& benchState) { + while (benchState.KeepRunning()) { + auto displayList = new CanvasOpBuffer(); + benchmark::DoNotOptimize(displayList); + delete displayList; + } +} +BENCHMARK(BM_CanvasOpBuffer_alloc); + +void BM_CanvasOpBuffer_record_saverestore(benchmark::State& benchState) { + CanvasFrontend<CanvasOpBuffer> canvas(100, 100); + while (benchState.KeepRunning()) { + canvas.reset(100, 100); + canvas.save(SaveFlags::MatrixClip); + canvas.save(SaveFlags::MatrixClip); + benchmark::DoNotOptimize(&canvas); + canvas.restore(); + canvas.restore(); + canvas.finish(); + } +} +BENCHMARK(BM_CanvasOpBuffer_record_saverestore); + +void BM_CanvasOpBuffer_record_saverestoreWithReuse(benchmark::State& benchState) { + CanvasFrontend<CanvasOpBuffer> canvas(100, 100); + + while (benchState.KeepRunning()) { + canvas.reset(100, 100); + canvas.save(SaveFlags::MatrixClip); + canvas.save(SaveFlags::MatrixClip); + benchmark::DoNotOptimize(&canvas); + canvas.restore(); + canvas.restore(); + } +} +BENCHMARK(BM_CanvasOpBuffer_record_saverestoreWithReuse); + +void BM_CanvasOpBuffer_record_simpleBitmapView(benchmark::State& benchState) { + CanvasFrontend<CanvasOpBuffer> canvas(100, 100); + + Paint rectPaint; + sk_sp<Bitmap> iconBitmap(TestUtils::createBitmap(80, 80)); + + while (benchState.KeepRunning()) { + canvas.reset(100, 100); + { + canvas.save(SaveFlags::MatrixClip); + canvas.draw(CanvasOp<CanvasOpType::DrawRect> { + .rect = SkRect::MakeWH(100, 100), + .paint = rectPaint, + }); + canvas.restore(); + } + { + canvas.save(SaveFlags::MatrixClip); + canvas.translate(10, 10); + canvas.draw(CanvasOp<CanvasOpType::DrawImage> { + iconBitmap, + 0, + 0, + SkFilterMode::kNearest, + SkPaint{} + }); + canvas.restore(); + } + benchmark::DoNotOptimize(&canvas); + canvas.finish(); + } +} +BENCHMARK(BM_CanvasOpBuffer_record_simpleBitmapView); diff --git a/libs/hwui/tests/microbench/DisplayListCanvasBench.cpp b/libs/hwui/tests/microbench/DisplayListCanvasBench.cpp index 4ce6c32470ea..9cd10759a834 100644 --- a/libs/hwui/tests/microbench/DisplayListCanvasBench.cpp +++ b/libs/hwui/tests/microbench/DisplayListCanvasBench.cpp @@ -24,40 +24,41 @@ using namespace android; using namespace android::uirenderer; +using namespace android::uirenderer::skiapipeline; -void BM_DisplayList_alloc(benchmark::State& benchState) { +void BM_SkiaDisplayList_alloc(benchmark::State& benchState) { while (benchState.KeepRunning()) { auto displayList = new skiapipeline::SkiaDisplayList(); benchmark::DoNotOptimize(displayList); delete displayList; } } -BENCHMARK(BM_DisplayList_alloc); +BENCHMARK(BM_SkiaDisplayList_alloc); -void BM_DisplayList_alloc_theoretical(benchmark::State& benchState) { +void BM_SkiaDisplayList_alloc_theoretical(benchmark::State& benchState) { while (benchState.KeepRunning()) { auto displayList = new char[sizeof(skiapipeline::SkiaDisplayList)]; benchmark::DoNotOptimize(displayList); delete[] displayList; } } -BENCHMARK(BM_DisplayList_alloc_theoretical); +BENCHMARK(BM_SkiaDisplayList_alloc_theoretical); -void BM_DisplayListCanvas_record_empty(benchmark::State& benchState) { - std::unique_ptr<Canvas> canvas(Canvas::create_recording_canvas(100, 100)); - delete canvas->finishRecording(); +void BM_SkiaDisplayListCanvas_record_empty(benchmark::State& benchState) { + auto canvas = std::make_unique<SkiaRecordingCanvas>(nullptr, 100, 100); + static_cast<void>(canvas->finishRecording()); while (benchState.KeepRunning()) { canvas->resetRecording(100, 100); benchmark::DoNotOptimize(canvas.get()); - delete canvas->finishRecording(); + static_cast<void>(canvas->finishRecording()); } } -BENCHMARK(BM_DisplayListCanvas_record_empty); +BENCHMARK(BM_SkiaDisplayListCanvas_record_empty); -void BM_DisplayListCanvas_record_saverestore(benchmark::State& benchState) { - std::unique_ptr<Canvas> canvas(Canvas::create_recording_canvas(100, 100)); - delete canvas->finishRecording(); +void BM_SkiaDisplayListCanvas_record_saverestore(benchmark::State& benchState) { + auto canvas = std::make_unique<SkiaRecordingCanvas>(nullptr, 100, 100); + static_cast<void>(canvas->finishRecording()); while (benchState.KeepRunning()) { canvas->resetRecording(100, 100); @@ -66,23 +67,23 @@ void BM_DisplayListCanvas_record_saverestore(benchmark::State& benchState) { benchmark::DoNotOptimize(canvas.get()); canvas->restore(); canvas->restore(); - delete canvas->finishRecording(); + static_cast<void>(canvas->finishRecording()); } } -BENCHMARK(BM_DisplayListCanvas_record_saverestore); +BENCHMARK(BM_SkiaDisplayListCanvas_record_saverestore); -void BM_DisplayListCanvas_record_translate(benchmark::State& benchState) { - std::unique_ptr<Canvas> canvas(Canvas::create_recording_canvas(100, 100)); - delete canvas->finishRecording(); +void BM_SkiaDisplayListCanvas_record_translate(benchmark::State& benchState) { + auto canvas = std::make_unique<SkiaRecordingCanvas>(nullptr, 100, 100); + static_cast<void>(canvas->finishRecording()); while (benchState.KeepRunning()) { canvas->resetRecording(100, 100); canvas->scale(10, 10); benchmark::DoNotOptimize(canvas.get()); - delete canvas->finishRecording(); + static_cast<void>(canvas->finishRecording()); } } -BENCHMARK(BM_DisplayListCanvas_record_translate); +BENCHMARK(BM_SkiaDisplayListCanvas_record_translate); /** * Simulate a simple view drawing a background, overlapped by an image. @@ -90,9 +91,9 @@ BENCHMARK(BM_DisplayListCanvas_record_translate); * Note that the recording commands are intentionally not perfectly efficient, as the * View system frequently produces unneeded save/restores. */ -void BM_DisplayListCanvas_record_simpleBitmapView(benchmark::State& benchState) { - std::unique_ptr<Canvas> canvas(Canvas::create_recording_canvas(100, 100)); - delete canvas->finishRecording(); +void BM_SkiaDisplayListCanvas_record_simpleBitmapView(benchmark::State& benchState) { + auto canvas = std::make_unique<SkiaRecordingCanvas>(nullptr, 100, 100); + static_cast<void>(canvas->finishRecording()); Paint rectPaint; sk_sp<Bitmap> iconBitmap(TestUtils::createBitmap(80, 80)); @@ -111,18 +112,18 @@ void BM_DisplayListCanvas_record_simpleBitmapView(benchmark::State& benchState) canvas->restore(); } benchmark::DoNotOptimize(canvas.get()); - delete canvas->finishRecording(); + static_cast<void>(canvas->finishRecording()); } } -BENCHMARK(BM_DisplayListCanvas_record_simpleBitmapView); +BENCHMARK(BM_SkiaDisplayListCanvas_record_simpleBitmapView); -void BM_DisplayListCanvas_basicViewGroupDraw(benchmark::State& benchState) { +void BM_SkiaDisplayListCanvas_basicViewGroupDraw(benchmark::State& benchState) { sp<RenderNode> child = TestUtils::createNode(50, 50, 100, 100, [](auto& props, auto& canvas) { canvas.drawColor(0xFFFFFFFF, SkBlendMode::kSrcOver); }); - std::unique_ptr<Canvas> canvas(Canvas::create_recording_canvas(100, 100)); - delete canvas->finishRecording(); + auto canvas = std::make_unique<SkiaRecordingCanvas>(nullptr, 100, 100); + static_cast<void>(canvas->finishRecording()); while (benchState.KeepRunning()) { canvas->resetRecording(200, 200); @@ -133,17 +134,17 @@ 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(); + static_cast<void>(canvas->finishRecording()); } } -BENCHMARK(BM_DisplayListCanvas_basicViewGroupDraw)->Arg(1)->Arg(5)->Arg(10); +BENCHMARK(BM_SkiaDisplayListCanvas_basicViewGroupDraw)->Arg(1)->Arg(5)->Arg(10); diff --git a/libs/hwui/tests/microbench/RenderNodeBench.cpp b/libs/hwui/tests/microbench/RenderNodeBench.cpp index 206dcd58d785..6aed251481bf 100644 --- a/libs/hwui/tests/microbench/RenderNodeBench.cpp +++ b/libs/hwui/tests/microbench/RenderNodeBench.cpp @@ -16,6 +16,7 @@ #include <benchmark/benchmark.h> +#include "hwui/Canvas.h" #include "RenderNode.h" using namespace android; @@ -30,3 +31,16 @@ void BM_RenderNode_create(benchmark::State& state) { } } BENCHMARK(BM_RenderNode_create); + +void BM_RenderNode_recordSimple(benchmark::State& state) { + sp<RenderNode> node = new RenderNode(); + std::unique_ptr<Canvas> canvas(Canvas::create_recording_canvas(100, 100)); + canvas->finishRecording(node.get()); + + while (state.KeepRunning()) { + canvas->resetRecording(100, 100, node.get()); + canvas->drawColor(0x00000000, SkBlendMode::kSrcOver); + canvas->finishRecording(node.get()); + } +} +BENCHMARK(BM_RenderNode_recordSimple); 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/CanvasFrontendTests.cpp b/libs/hwui/tests/unit/CanvasFrontendTests.cpp new file mode 100644 index 000000000000..05b11795d90d --- /dev/null +++ b/libs/hwui/tests/unit/CanvasFrontendTests.cpp @@ -0,0 +1,213 @@ +/* + * 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 <gtest/gtest.h> + +#include <canvas/CanvasFrontend.h> +#include <canvas/CanvasOpBuffer.h> +#include <canvas/CanvasOps.h> +#include <canvas/CanvasOpRasterizer.h> + +#include <tests/common/CallCountingCanvas.h> + +#include "SkPictureRecorder.h" +#include "SkColor.h" +#include "SkLatticeIter.h" +#include "pipeline/skia/AnimatedDrawables.h" +#include <SkNoDrawCanvas.h> + +using namespace android; +using namespace android::uirenderer; +using namespace android::uirenderer::test; + +class CanvasOpCountingReceiver { +public: + template <CanvasOpType T> + void push_container(CanvasOpContainer<T>&& op) { + mOpCounts[static_cast<size_t>(T)] += 1; + } + + int operator[](CanvasOpType op) const { + return mOpCounts[static_cast<size_t>(op)]; + } + +private: + std::array<int, static_cast<size_t>(CanvasOpType::COUNT)> mOpCounts; +}; + +TEST(CanvasFrontend, saveCount) { + SkNoDrawCanvas skiaCanvas(100, 100); + CanvasFrontend<CanvasOpCountingReceiver> opCanvas(100, 100); + const auto& receiver = opCanvas.receiver(); + + EXPECT_EQ(1, skiaCanvas.getSaveCount()); + EXPECT_EQ(1, opCanvas.saveCount()); + + skiaCanvas.save(); + opCanvas.save(SaveFlags::MatrixClip); + EXPECT_EQ(2, skiaCanvas.getSaveCount()); + EXPECT_EQ(2, opCanvas.saveCount()); + + skiaCanvas.restore(); + opCanvas.restore(); + EXPECT_EQ(1, skiaCanvas.getSaveCount()); + EXPECT_EQ(1, opCanvas.saveCount()); + + skiaCanvas.restore(); + opCanvas.restore(); + EXPECT_EQ(1, skiaCanvas.getSaveCount()); + EXPECT_EQ(1, opCanvas.saveCount()); + + EXPECT_EQ(1, receiver[CanvasOpType::Save]); + EXPECT_EQ(1, receiver[CanvasOpType::Restore]); +} + +TEST(CanvasFrontend, transform) { + SkNoDrawCanvas skiaCanvas(100, 100); + CanvasFrontend<CanvasOpCountingReceiver> opCanvas(100, 100); + + skiaCanvas.translate(10, 10); + opCanvas.translate(10, 10); + EXPECT_EQ(skiaCanvas.getTotalMatrix(), opCanvas.transform()); + + { + skiaCanvas.save(); + opCanvas.save(SaveFlags::Matrix); + skiaCanvas.scale(2.0f, 1.125f); + opCanvas.scale(2.0f, 1.125f); + + EXPECT_EQ(skiaCanvas.getTotalMatrix(), opCanvas.transform()); + skiaCanvas.restore(); + opCanvas.restore(); + } + + EXPECT_EQ(skiaCanvas.getTotalMatrix(), opCanvas.transform()); + + { + skiaCanvas.save(); + opCanvas.save(SaveFlags::Matrix); + skiaCanvas.rotate(90.f); + opCanvas.rotate(90.f); + + EXPECT_EQ(skiaCanvas.getTotalMatrix(), opCanvas.transform()); + + { + skiaCanvas.save(); + opCanvas.save(SaveFlags::Matrix); + skiaCanvas.skew(5.0f, 2.25f); + opCanvas.skew(5.0f, 2.25f); + + EXPECT_EQ(skiaCanvas.getTotalMatrix(), opCanvas.transform()); + skiaCanvas.restore(); + opCanvas.restore(); + } + + skiaCanvas.restore(); + opCanvas.restore(); + } + + EXPECT_EQ(skiaCanvas.getTotalMatrix(), opCanvas.transform()); +} + +TEST(CanvasFrontend, drawOpTransform) { + CanvasFrontend<CanvasOpBuffer> opCanvas(100, 100); + const auto& receiver = opCanvas.receiver(); + + auto makeDrawRect = [] { + return CanvasOp<CanvasOpType::DrawRect>{ + .rect = SkRect::MakeWH(50, 50), + .paint = SkPaint(SkColors::kBlack), + }; + }; + + opCanvas.draw(makeDrawRect()); + + opCanvas.translate(10, 10); + opCanvas.draw(makeDrawRect()); + + opCanvas.save(); + opCanvas.scale(2.0f, 4.0f); + opCanvas.draw(makeDrawRect()); + opCanvas.restore(); + + opCanvas.save(); + opCanvas.translate(20, 15); + opCanvas.draw(makeDrawRect()); + opCanvas.save(); + opCanvas.rotate(90.f); + opCanvas.draw(makeDrawRect()); + opCanvas.restore(); + opCanvas.restore(); + + // Validate the results + std::vector<SkMatrix> transforms; + transforms.reserve(5); + receiver.for_each([&](auto op) { + // Filter for the DrawRect calls; ignore the save & restores + // (TODO: Add a filtered for_each variant to OpBuffer?) + if (op->type() == CanvasOpType::DrawRect) { + transforms.push_back(op->transform()); + } + }); + + EXPECT_EQ(transforms.size(), 5); + + { + // First result should be identity + const auto& result = transforms[0]; + EXPECT_EQ(SkMatrix::kIdentity_Mask, result.getType()); + EXPECT_EQ(SkMatrix::I(), result); + } + + { + // Should be translate 10, 10 + const auto& result = transforms[1]; + EXPECT_EQ(SkMatrix::kTranslate_Mask, result.getType()); + SkMatrix m; + m.setTranslate(10, 10); + EXPECT_EQ(m, result); + } + + { + // Should be translate 10, 10 + scale 2, 4 + const auto& result = transforms[2]; + EXPECT_EQ(SkMatrix::kTranslate_Mask | SkMatrix::kScale_Mask, result.getType()); + SkMatrix m; + m.setTranslate(10, 10); + m.preScale(2.0f, 4.0f); + EXPECT_EQ(m, result); + } + + { + // Should be translate 10, 10 + translate 20, 15 + const auto& result = transforms[3]; + EXPECT_EQ(SkMatrix::kTranslate_Mask, result.getType()); + SkMatrix m; + m.setTranslate(30, 25); + EXPECT_EQ(m, result); + } + + { + // Should be translate 10, 10 + translate 20, 15 + rotate 90 + const auto& result = transforms[4]; + EXPECT_EQ(SkMatrix::kTranslate_Mask | SkMatrix::kAffine_Mask | SkMatrix::kScale_Mask, + result.getType()); + SkMatrix m; + m.setTranslate(30, 25); + m.preRotate(90.f); + EXPECT_EQ(m, result); + } +}
\ No newline at end of file diff --git a/libs/hwui/tests/unit/CanvasOpTests.cpp b/libs/hwui/tests/unit/CanvasOpTests.cpp new file mode 100644 index 000000000000..54970df534b9 --- /dev/null +++ b/libs/hwui/tests/unit/CanvasOpTests.cpp @@ -0,0 +1,612 @@ +/* + * 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 <gtest/gtest.h> + +#include <canvas/CanvasFrontend.h> +#include <canvas/CanvasOpBuffer.h> +#include <canvas/CanvasOps.h> +#include <canvas/CanvasOpRasterizer.h> + +#include <tests/common/CallCountingCanvas.h> + +#include "SkPictureRecorder.h" +#include "SkColor.h" +#include "SkLatticeIter.h" +#include "pipeline/skia/AnimatedDrawables.h" +#include <SkNoDrawCanvas.h> + +using namespace android; +using namespace android::uirenderer; +using namespace android::uirenderer::test; + +// We lazy +using Op = CanvasOpType; + +class CanvasOpCountingReceiver { +public: + template <CanvasOpType T> + void push_container(CanvasOpContainer<T>&& op) { + mOpCounts[static_cast<size_t>(T)] += 1; + } + + int operator[](CanvasOpType op) const { + return mOpCounts[static_cast<size_t>(op)]; + } + +private: + std::array<int, static_cast<size_t>(CanvasOpType::COUNT)> mOpCounts; +}; + +template<typename T> +static int countItems(const T& t) { + int count = 0; + t.for_each([&](auto i) { + count++; + }); + return count; +} + +TEST(CanvasOp, verifyConst) { + CanvasOpBuffer buffer; + buffer.push<Op::DrawColor>({ + .color = SkColors::kBlack, + .mode = SkBlendMode::kSrcOver, + }); + buffer.for_each([](auto op) { + static_assert(std::is_const_v<std::remove_reference_t<decltype(*op)>>, + "Expected container to be const"); + static_assert(std::is_const_v<std::remove_reference_t<decltype(op->op())>>, + "Expected op to be const"); + }); +} + +TEST(CanvasOp, simplePush) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + buffer.push<Op::Save>({}); + buffer.push<Op::Save>({}); + buffer.push<Op::Restore>({}); + EXPECT_GT(buffer.size(), 0); + + int saveCount = 0; + int restoreCount = 0; + int otherCount = 0; + + buffer.for_each([&](auto op) { + switch (op->type()) { + case Op::Save: + saveCount++; + break; + case Op::Restore: + restoreCount++; + break; + default: + otherCount++; + break; + } + }); + + EXPECT_EQ(saveCount, 2); + EXPECT_EQ(restoreCount, 1); + EXPECT_EQ(otherCount, 0); + + buffer.clear(); + int itemCount = 0; + buffer.for_each([&](auto op) { + itemCount++; + }); + EXPECT_EQ(itemCount, 0); + buffer.resize(0); + EXPECT_EQ(buffer.size(), 0); +} + +TEST(CanvasOp, simpleDrawPaint) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + buffer.push<Op::DrawColor> ({ + .color = SkColor4f{1, 1, 1, 1}, + .mode = SkBlendMode::kSrcIn + }); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawPaintCount); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, simpleDrawPoint) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + buffer.push<Op::DrawPoint> ({ + .x = 12, + .y = 42, + .paint = SkPaint{} + }); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawPoints); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, simpleDrawPoints) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + size_t numPts = 3; + auto pts = sk_ref_sp( + new Points({ + {32, 16}, + {48, 48}, + {16, 32} + }) + ); + + buffer.push(CanvasOp<Op::DrawPoints> { + .count = numPts, + .paint = SkPaint{}, + .points = pts + }); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawPoints); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, simpleDrawLine) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + buffer.push<Op::DrawLine> ({ + .startX = 16, + .startY = 28, + .endX = 12, + .endY = 30, + .paint = SkPaint{} + }); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawPoints); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, simpleDrawLines) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + size_t numPts = 3; + auto pts = sk_ref_sp( + new Points({ + {32, 16}, + {48, 48}, + {16, 32} + }) + ); + buffer.push(CanvasOp<Op::DrawLines> { + .count = numPts, + .paint = SkPaint{}, + .points = pts + }); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawPoints); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, simpleDrawRect) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + buffer.push<Op::DrawRect> ({ + .paint = SkPaint{}, + .rect = SkRect::MakeEmpty() + }); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawRectCount); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, simpleDrawRegionRect) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + SkRegion region; + region.setRect(SkIRect::MakeWH(12, 50)); + buffer.push<Op::DrawRegion> ({ + .paint = SkPaint{}, + .region = region + }); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + // If the region is a rectangle, drawRegion calls into drawRect as a fast path + EXPECT_EQ(1, canvas.drawRectCount); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, simpleDrawRegionPath) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + SkPath path; + path.addCircle(50, 50, 50); + SkRegion clip; + clip.setRect(SkIRect::MakeWH(100, 100)); + SkRegion region; + region.setPath(path, clip); + buffer.push<Op::DrawRegion> ({ + .paint = SkPaint{}, + .region = region + }); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawRegionCount); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, simpleDrawRoundRect) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + buffer.push<Op::DrawRoundRect> ({ + .paint = SkPaint{}, + .rect = SkRect::MakeEmpty(), + .rx = 10, + .ry = 10 + }); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawRRectCount); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, simpleDrawDoubleRoundRect) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + SkRect outer = SkRect::MakeLTRB(0, 0, 100, 100); + SkRect inner = SkRect::MakeLTRB(20, 20, 80, 80); + + const int numPts = 4; + SkRRect outerRRect; + + auto outerPts = std::make_unique<SkVector[]>(numPts); + outerPts[0].set(32, 16); + outerPts[1].set(48, 48); + outerPts[2].set(16, 32); + outerPts[3].set(20, 20); + outerRRect.setRectRadii(outer, outerPts.get()); + outerRRect.setRect(outer); + + SkRRect innerRRect; + auto innerPts = std::make_unique<SkVector[]>(numPts); + innerPts[0].set(16, 8); + innerPts[1].set(24, 24); + innerPts[2].set(8, 16); + innerPts[3].set(10, 10); + innerRRect.setRectRadii(inner, innerPts.get()); + + buffer.push<Op::DrawDoubleRoundRect> ({ + .outer = outerRRect, + .inner = innerRRect, + .paint = SkPaint{} + }); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawDRRectCount); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, simpleDrawCircle) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + buffer.push<Op::DrawCircle>({ + .cx = 5, + .cy = 7, + .radius = 10, + .paint = SkPaint{} + }); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawOvalCount); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, simpleDrawOval) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + buffer.push<Op::DrawOval> ({ + .oval = SkRect::MakeEmpty(), + .paint = SkPaint{} + }); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawOvalCount); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, simpleDrawArc) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + buffer.push<Op::DrawArc>({ + .oval = SkRect::MakeWH(100, 100), + .startAngle = 120, + .sweepAngle = 70, + .useCenter = true, + .paint = SkPaint{} + }); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawArcCount); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, simpleDrawPath) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + SkPath path; + path.addCircle(50, 50, 30); + buffer.push<Op::DrawPath> ({ + .path = path, + .paint = SkPaint{} + }); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawPathCount); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, simpleDrawRoundRectProperty) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + + auto left = sp<CanvasPropertyPrimitive>(new uirenderer::CanvasPropertyPrimitive(1)); + auto top = sp<CanvasPropertyPrimitive>(new uirenderer::CanvasPropertyPrimitive(2)); + auto right = sp<CanvasPropertyPrimitive>(new uirenderer::CanvasPropertyPrimitive(3)); + auto bottom = sp<CanvasPropertyPrimitive>(new uirenderer::CanvasPropertyPrimitive(4)); + auto radiusX = sp<CanvasPropertyPrimitive>(new uirenderer::CanvasPropertyPrimitive(5)); + auto radiusY = sp<CanvasPropertyPrimitive>(new uirenderer::CanvasPropertyPrimitive(6)); + auto propertyPaint = + sp<uirenderer::CanvasPropertyPaint>(new uirenderer::CanvasPropertyPaint(SkPaint{})); + + buffer.push<Op::DrawRoundRectProperty> ({ + .left = left, + .top = top, + .right = right, + .bottom = bottom, + .rx = radiusX, + .ry = radiusY, + .paint = propertyPaint + }); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawRRectCount); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, simpleDrawCircleProperty) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + + auto x = sp<CanvasPropertyPrimitive>(new uirenderer::CanvasPropertyPrimitive(1)); + auto y = sp<CanvasPropertyPrimitive>(new uirenderer::CanvasPropertyPrimitive(2)); + auto radius = sp<CanvasPropertyPrimitive>(new uirenderer::CanvasPropertyPrimitive(5)); + auto propertyPaint = + sp<uirenderer::CanvasPropertyPaint>(new uirenderer::CanvasPropertyPaint(SkPaint{})); + + buffer.push<Op::DrawCircleProperty> ({ + .x = x, + .y = y, + .radius = radius, + .paint = propertyPaint + }); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawOvalCount); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, simpleDrawVertices) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + + SkPoint pts[3] = {{64, 32}, {0, 224}, {128, 224}}; + SkColor colors[3] = {SK_ColorRED, SK_ColorBLUE, SK_ColorGREEN}; + sk_sp<SkVertices> vertices = SkVertices::MakeCopy(SkVertices::kTriangles_VertexMode, 3, pts, + nullptr, colors); + buffer.push<Op::DrawVertices> ({ + .vertices = vertices, + .mode = SkBlendMode::kSrcOver, + .paint = SkPaint{} + }); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawVerticesCount); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, simpleDrawImage) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + + SkImageInfo info =SkImageInfo::Make(5, 1, + kGray_8_SkColorType, kOpaque_SkAlphaType); + sk_sp<Bitmap> bitmap = Bitmap::allocateHeapBitmap(info); + buffer.push<Op::DrawImage> ({ + bitmap, + 7, + 19, + SkFilterMode::kNearest, + SkPaint{} + } + ); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawImageCount); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, simpleDrawImageRect) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + + SkImageInfo info = SkImageInfo::Make(5, 1, + kGray_8_SkColorType, kOpaque_SkAlphaType); + + sk_sp<Bitmap> bitmap = Bitmap::allocateHeapBitmap(info); + buffer.push<Op::DrawImageRect> ({ + bitmap, SkRect::MakeWH(100, 100), + SkRect::MakeLTRB(120, 110, 220, 210), + SkFilterMode::kNearest, SkPaint{} + } + ); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawImageRectCount); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, simpleDrawImageLattice) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + + SkBitmap skBitmap; + skBitmap.allocPixels(SkImageInfo::MakeN32Premul(60, 60)); + + const int xDivs[] = { 20, 50 }; + const int yDivs[] = { 10, 40 }; + SkCanvas::Lattice::RectType fillTypes[3][3]; + memset(fillTypes, 0, sizeof(fillTypes)); + fillTypes[1][1] = SkCanvas::Lattice::kTransparent; + SkColor colors[9]; + SkCanvas::Lattice lattice = { xDivs, yDivs, fillTypes[0], 2, + 2, nullptr, colors }; + sk_sp<Bitmap> bitmap = Bitmap::allocateHeapBitmap(&skBitmap); + buffer.push<Op::DrawImageLattice>( + { + bitmap, + SkRect::MakeWH(5, 1), + lattice, + SkFilterMode::kNearest, + SkPaint{} + } + ); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + EXPECT_EQ(1, canvas.drawImageLatticeCount); + EXPECT_EQ(1, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, simpleDrawPicture) { + CanvasOpBuffer buffer; + EXPECT_EQ(buffer.size(), 0); + + SkPictureRecorder recorder; + SkCanvas* pictureCanvas = recorder.beginRecording({64, 64, 192, 192}); + SkPaint paint; + pictureCanvas->drawRect(SkRect::MakeWH(200, 200), paint); + paint.setColor(SK_ColorWHITE); + pictureCanvas->drawRect(SkRect::MakeLTRB(20, 20, 180, 180), paint); + sk_sp<SkPicture> picture = recorder.finishRecordingAsPicture(); + buffer.push<Op::DrawPicture> ({ + .picture = picture + }); + + CallCountingCanvas canvas; + EXPECT_EQ(0, canvas.sumTotalDrawCalls()); + rasterizeCanvasBuffer(buffer, &canvas); + // Note because we are explicitly issuing 2 drawRect calls + // in the picture recorder above, when it is played back into + // CallCountingCanvas we will see 2 calls to drawRect instead of 1 + // call to drawPicture. + // This is because SkiaCanvas::drawPicture uses picture.playback(canvas) + // instead of canvas->drawPicture. + EXPECT_EQ(2, canvas.drawRectCount); + EXPECT_EQ(2, canvas.sumTotalDrawCalls()); +} + +TEST(CanvasOp, immediateRendering) { + auto canvas = std::make_shared<CallCountingCanvas>(); + + EXPECT_EQ(0, canvas->sumTotalDrawCalls()); + ImmediateModeRasterizer rasterizer{canvas}; + auto op = CanvasOp<Op::DrawRect> { + .paint = SkPaint{}, + .rect = SkRect::MakeEmpty() + }; + EXPECT_TRUE(CanvasOpTraits::can_draw<decltype(op)>); + rasterizer.draw(op); + EXPECT_EQ(1, canvas->drawRectCount); + EXPECT_EQ(1, canvas->sumTotalDrawCalls()); +} + +TEST(CanvasOp, frontendSaveCount) { + SkNoDrawCanvas skiaCanvas(100, 100); + CanvasFrontend<CanvasOpCountingReceiver> opCanvas(100, 100); + const auto& receiver = opCanvas.receiver(); + + EXPECT_EQ(1, skiaCanvas.getSaveCount()); + EXPECT_EQ(1, opCanvas.saveCount()); + + skiaCanvas.save(); + opCanvas.save(SaveFlags::MatrixClip); + EXPECT_EQ(2, skiaCanvas.getSaveCount()); + EXPECT_EQ(2, opCanvas.saveCount()); + + skiaCanvas.restore(); + opCanvas.restore(); + EXPECT_EQ(1, skiaCanvas.getSaveCount()); + EXPECT_EQ(1, opCanvas.saveCount()); + + skiaCanvas.restore(); + opCanvas.restore(); + EXPECT_EQ(1, skiaCanvas.getSaveCount()); + EXPECT_EQ(1, opCanvas.saveCount()); + + EXPECT_EQ(1, receiver[Op::Save]); + EXPECT_EQ(1, receiver[Op::Restore]); +} diff --git a/libs/hwui/tests/unit/CommonPoolTests.cpp b/libs/hwui/tests/unit/CommonPoolTests.cpp index da6a2604a4b6..bffdeca4db54 100644 --- a/libs/hwui/tests/unit/CommonPoolTests.cpp +++ b/libs/hwui/tests/unit/CommonPoolTests.cpp @@ -54,7 +54,9 @@ TEST(DISABLED_CommonPool, threadCount) { EXPECT_EQ(0, threads.count(gettid())); } -TEST(CommonPool, singleThread) { +// Disabled since this is flaky. This isn't a necessarily useful functional test, so being +// disabled isn't that significant. However it may be good to resurrect this somehow. +TEST(CommonPool, DISABLED_singleThread) { std::mutex mutex; std::condition_variable fence; bool isProcessing = false; diff --git a/libs/hwui/tests/unit/DeferredLayerUpdaterTests.cpp b/libs/hwui/tests/unit/DeferredLayerUpdaterTests.cpp index f4c3e13b0ea6..955a5e7d8b3a 100644 --- a/libs/hwui/tests/unit/DeferredLayerUpdaterTests.cpp +++ b/libs/hwui/tests/unit/DeferredLayerUpdaterTests.cpp @@ -39,7 +39,7 @@ RENDERTHREAD_TEST(DeferredLayerUpdater, updateLayer) { EXPECT_EQ(Matrix4::identity(), layerUpdater->backingLayer()->getTexTransform()); // push the deferred updates to the layer - SkMatrix scaledMatrix = SkMatrix::MakeScale(0.5, 0.5); + SkMatrix scaledMatrix = SkMatrix::Scale(0.5, 0.5); SkBitmap bitmap; bitmap.allocN32Pixels(16, 16); sk_sp<SkImage> layerImage = SkImage::MakeFromBitmap(bitmap); diff --git a/libs/hwui/tests/unit/FatalTestCanvas.h b/libs/hwui/tests/unit/FatalTestCanvas.h index 1723c2eb4948..2a74afc5bb7a 100644 --- a/libs/hwui/tests/unit/FatalTestCanvas.h +++ b/libs/hwui/tests/unit/FatalTestCanvas.h @@ -60,42 +60,25 @@ public: void onDrawVerticesObject(const SkVertices*, SkBlendMode, const SkPaint&) { ADD_FAILURE() << "onDrawVertices not expected in this test"; } - void onDrawAtlas(const SkImage*, const SkRSXform[], const SkRect[], const SkColor[], int count, - SkBlendMode, const SkRect* cull, const SkPaint*) { + void onDrawAtlas2(const SkImage*, const SkRSXform[], const SkRect[], const SkColor[], int count, + SkBlendMode, const SkSamplingOptions&, const SkRect* cull, const SkPaint*) { ADD_FAILURE() << "onDrawAtlas not expected in this test"; } void onDrawPath(const SkPath&, const SkPaint&) { ADD_FAILURE() << "onDrawPath not expected in this test"; } - void onDrawImage(const SkImage*, SkScalar dx, SkScalar dy, const SkPaint*) { + void onDrawImage2(const SkImage*, SkScalar dx, SkScalar dy, const SkSamplingOptions&, + const SkPaint*) { ADD_FAILURE() << "onDrawImage not expected in this test"; } - void onDrawImageRect(const SkImage*, const SkRect*, const SkRect&, const SkPaint*, - SrcRectConstraint) { + void onDrawImageRect2(const SkImage*, const SkRect&, const SkRect&, const SkSamplingOptions&, + const SkPaint*, SrcRectConstraint) { ADD_FAILURE() << "onDrawImageRect not expected in this test"; } - void onDrawImageNine(const SkImage*, const SkIRect& center, const SkRect& dst, const SkPaint*) { - ADD_FAILURE() << "onDrawImageNine not expected in this test"; - } - void onDrawImageLattice(const SkImage*, const Lattice& lattice, const SkRect& dst, - const SkPaint*) { + void onDrawImageLattice2(const SkImage*, const Lattice& lattice, const SkRect& dst, + SkFilterMode, 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/OpBufferTests.cpp b/libs/hwui/tests/unit/OpBufferTests.cpp new file mode 100644 index 000000000000..c0ae943dddee --- /dev/null +++ b/libs/hwui/tests/unit/OpBufferTests.cpp @@ -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. + */ + +#include <gtest/gtest.h> + +#include <canvas/OpBuffer.h> + +using namespace android; +using namespace android::uirenderer; + +enum MockTypes { + Lifecycle, + NoOp, + IntHolder, + COUNT +}; + +using Op = MockTypes; + +template<MockTypes T> +struct MockOp; + +template<MockTypes T> +struct MockOpContainer { + OpBufferItemHeader<MockTypes> header; + MockOp<T> impl; + + MockOpContainer(MockOp<T>&& impl) : impl(std::move(impl)) {} +}; + +struct LifecycleTracker { + int ctor_count = 0; + int dtor_count = 0; + + int alive() { return ctor_count - dtor_count; } +}; + +template<> +struct MockOp<MockTypes::Lifecycle> { + MockOp() = delete; + void operator=(const MockOp&) = delete; + + MockOp(LifecycleTracker* tracker) : tracker(tracker) { + tracker->ctor_count += 1; + } + + MockOp(const MockOp& other) { + tracker = other.tracker; + tracker->ctor_count += 1; + } + + ~MockOp() { + tracker->dtor_count += 1; + } + + LifecycleTracker* tracker = nullptr; +}; + +template<> +struct MockOp<MockTypes::NoOp> {}; + +template<> +struct MockOp<MockTypes::IntHolder> { + int value = -1; +}; + +struct MockBuffer : public OpBuffer<MockTypes, MockOpContainer> { + template <MockTypes T> + void push(MockOp<T>&& op) { + push_container(MockOpContainer<T>{std::move(op)}); + } +}; + +template<typename T> +static int countItems(const T& t) { + int count = 0; + t.for_each([&](auto i) { + count++; + }); + return count; +} + +TEST(OpBuffer, lifecycleCheck) { + LifecycleTracker tracker; + { + MockBuffer buffer; + buffer.push_container(MockOpContainer<Op::Lifecycle> { + MockOp<MockTypes::Lifecycle>{&tracker} + }); + EXPECT_EQ(tracker.alive(), 1); + buffer.clear(); + EXPECT_EQ(tracker.alive(), 0); + } + EXPECT_EQ(tracker.alive(), 0); +} + +TEST(OpBuffer, lifecycleCheckMove) { + LifecycleTracker tracker; + { + MockBuffer buffer; + buffer.push_container(MockOpContainer<Op::Lifecycle> { + MockOp<MockTypes::Lifecycle>{&tracker} + }); + EXPECT_EQ(tracker.alive(), 1); + { + MockBuffer other(std::move(buffer)); + EXPECT_EQ(tracker.alive(), 1); + EXPECT_EQ(buffer.size(), 0); + EXPECT_GT(other.size(), 0); + EXPECT_EQ(1, countItems(other)); + EXPECT_EQ(0, countItems(buffer)); + + other.push_container(MockOpContainer<MockTypes::Lifecycle> { + MockOp<MockTypes::Lifecycle>{&tracker} + }); + + EXPECT_EQ(2, countItems(other)); + EXPECT_EQ(2, tracker.alive()); + + buffer.push_container(MockOpContainer<MockTypes::Lifecycle> { + MockOp<MockTypes::Lifecycle>{&tracker} + }); + EXPECT_EQ(1, countItems(buffer)); + EXPECT_EQ(3, tracker.alive()); + + buffer = std::move(other); + EXPECT_EQ(2, countItems(buffer)); + EXPECT_EQ(2, tracker.alive()); + } + EXPECT_EQ(2, countItems(buffer)); + EXPECT_EQ(2, tracker.alive()); + buffer.clear(); + EXPECT_EQ(0, countItems(buffer)); + EXPECT_EQ(0, tracker.alive()); + } + EXPECT_EQ(tracker.alive(), 0); +} + +TEST(OpBuffer, verifyConst) { + MockBuffer buffer; + buffer.push<Op::IntHolder>({42}); + buffer.for_each([](auto op) { + static_assert(std::is_const_v<std::remove_reference_t<decltype(*op)>>, + "Expected container to be const"); + }); +} + +TEST(OpBuffer, filterView) { + MockBuffer buffer; + buffer.push<Op::NoOp>({}); + buffer.push<Op::IntHolder>({0}); + buffer.push<Op::IntHolder>({1}); + buffer.push<Op::NoOp>({}); + buffer.push<Op::NoOp>({}); + buffer.push<Op::IntHolder>({2}); + buffer.push<Op::NoOp>({}); + buffer.push<Op::NoOp>({}); + buffer.push<Op::NoOp>({}); + buffer.push<Op::NoOp>({}); + + + int index = 0; + for (const auto& it : buffer.filter<Op::IntHolder>()) { + ASSERT_EQ(Op::IntHolder, it.header.type); + EXPECT_EQ(index, it.impl.value); + index++; + } + EXPECT_EQ(index, 3); + + int count = 0; + for (const auto& it : buffer.filter<Op::NoOp>()) { + ASSERT_EQ(Op::NoOp, it.header.type); + count++; + } + EXPECT_EQ(count, 7); +} + diff --git a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp index 3632be06c45f..423400eb8ff1 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); }); @@ -198,7 +198,7 @@ TEST(RenderNodeDrawable, saveLayerClipAndMatrixRestore) { EXPECT_TRUE(getRecorderMatrix(recorder).isIdentity()); // note we don't pass SaveFlags::MatrixClip, but matrix and clip will be saved - recorder.saveLayer(0, 0, 400, 400, &layerPaint, SaveFlags::ClipToLayer); + recorder.saveLayer(0, 0, 400, 400, &layerPaint); ASSERT_EQ(SkRect::MakeLTRB(0, 0, 400, 400), getRecorderClipBounds(recorder)); EXPECT_TRUE(getRecorderMatrix(recorder).isIdentity()); @@ -206,7 +206,7 @@ TEST(RenderNodeDrawable, saveLayerClipAndMatrixRestore) { ASSERT_EQ(SkRect::MakeLTRB(50, 50, 350, 350), getRecorderClipBounds(recorder)); recorder.translate(300.0f, 400.0f); - EXPECT_EQ(SkMatrix::MakeTrans(300.0f, 400.0f), getRecorderMatrix(recorder)); + EXPECT_EQ(SkMatrix::Translate(300.0f, 400.0f), getRecorderMatrix(recorder)); recorder.restore(); ASSERT_EQ(SkRect::MakeLTRB(0, 0, 400, 800), getRecorderClipBounds(recorder)); @@ -938,7 +938,8 @@ RENDERTHREAD_TEST(RenderNodeDrawable, simple) { void onDrawRect(const SkRect& rect, const SkPaint& paint) override { EXPECT_EQ(0, mDrawCounter++); } - void onDrawImage(const SkImage*, SkScalar dx, SkScalar dy, const SkPaint*) override { + void onDrawImage2(const SkImage*, SkScalar dx, SkScalar dy, const SkSamplingOptions&, + const SkPaint*) override { EXPECT_EQ(1, mDrawCounter++); } }; @@ -1047,7 +1048,7 @@ TEST(RenderNodeDrawable, renderNode) { EXPECT_EQ(2, canvas.mDrawCounter); } -// Verify that layers are composed with kLow_SkFilterQuality filter quality. +// Verify that layers are composed with linear filtering. RENDERTHREAD_SKIA_PIPELINE_TEST(RenderNodeDrawable, layerComposeQuality) { static const int CANVAS_WIDTH = 1; static const int CANVAS_HEIGHT = 1; @@ -1056,10 +1057,12 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(RenderNodeDrawable, layerComposeQuality) { class FrameTestCanvas : public TestCanvasBase { public: FrameTestCanvas() : TestCanvasBase(CANVAS_WIDTH, CANVAS_HEIGHT) {} - void onDrawImageRect(const SkImage* image, const SkRect* src, const SkRect& dst, - const SkPaint* paint, SrcRectConstraint constraint) override { + void onDrawImageRect2(const SkImage* image, const SkRect& src, const SkRect& dst, + const SkSamplingOptions& sampling, const SkPaint* paint, + SrcRectConstraint constraint) override { mDrawCounter++; - EXPECT_EQ(kLow_SkFilterQuality, paint->getFilterQuality()); + EXPECT_FALSE(sampling.useCubic); + EXPECT_EQ(SkFilterMode::kLinear, sampling.filter); } }; @@ -1107,27 +1110,27 @@ TEST(ReorderBarrierDrawable, testShadowMatrix) { EXPECT_EQ(dy, TRANSLATE_Y); } - virtual void didSetMatrix(const SkMatrix& matrix) override { + virtual void didSetM44(const SkM44& matrix) override { mDrawCounter++; // First invocation is EndReorderBarrierDrawable::drawShadow to apply shadow matrix. // Second invocation is preparing the matrix for an elevated RenderNodeDrawable. - EXPECT_TRUE(matrix.isIdentity()); + EXPECT_TRUE(matrix == SkM44()); EXPECT_TRUE(getTotalMatrix().isIdentity()); } - virtual void didConcat(const SkMatrix& matrix) override { + virtual void didConcat44(const SkM44& matrix) override { mDrawCounter++; if (mFirstDidConcat) { // First invocation is EndReorderBarrierDrawable::drawShadow to apply shadow matrix. mFirstDidConcat = false; - EXPECT_EQ(SkMatrix::MakeTrans(CASTER_X + TRANSLATE_X, CASTER_Y + TRANSLATE_Y), + EXPECT_EQ(SkM44::Translate(CASTER_X + TRANSLATE_X, CASTER_Y + TRANSLATE_Y), matrix); - EXPECT_EQ(SkMatrix::MakeTrans(CASTER_X + TRANSLATE_X, CASTER_Y + TRANSLATE_Y), + EXPECT_EQ(SkMatrix::Translate(CASTER_X + TRANSLATE_X, CASTER_Y + TRANSLATE_Y), getTotalMatrix()); } else { // Second invocation is preparing the matrix for an elevated RenderNodeDrawable. - EXPECT_EQ(SkMatrix::MakeTrans(TRANSLATE_X, TRANSLATE_Y), matrix); - EXPECT_EQ(SkMatrix::MakeTrans(TRANSLATE_X, TRANSLATE_Y), getTotalMatrix()); + EXPECT_EQ(SkM44::Translate(TRANSLATE_X, TRANSLATE_Y), matrix); + EXPECT_EQ(SkMatrix::Translate(TRANSLATE_X, TRANSLATE_Y), getTotalMatrix()); } } @@ -1142,7 +1145,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 +1155,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,8 +1172,9 @@ 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, - const SkPaint* paint, SrcRectConstraint constraint) override { + void onDrawImageRect2(const SkImage*, const SkRect& src, const SkRect& dst, + const SkSamplingOptions&, const SkPaint* paint, + SrcRectConstraint constraint) override { const int index = mDrawCounter++; switch (index) { case 0: diff --git a/libs/hwui/tests/unit/RenderNodeTests.cpp b/libs/hwui/tests/unit/RenderNodeTests.cpp index 1cd9bd8ee9d9..61bd646b0a76 100644 --- a/libs/hwui/tests/unit/RenderNodeTests.cpp +++ b/libs/hwui/tests/unit/RenderNodeTests.cpp @@ -231,39 +231,43 @@ 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); + }); + // Fence on any remaining post'd work + TestUtils::runOnRenderThreadUnmanaged([] (RenderThread&) {}); + EXPECT_EQ(2, counts.sync); + EXPECT_EQ(1, counts.destroyed); } RENDERTHREAD_TEST(RenderNode, prepareTree_nullableDisplayList) { @@ -322,7 +326,7 @@ RENDERTHREAD_TEST(DISABLED_RenderNode, prepareTree_HwLayer_AVD_enqueueDamage) { // Check that the VD is in the dislay list, and the layer update queue contains the correct // damage rect. - EXPECT_TRUE(rootNode->getDisplayList()->hasVectorDrawables()); + EXPECT_TRUE(rootNode->getDisplayList().hasVectorDrawables()); ASSERT_FALSE(info.layerUpdateQueue->entries().empty()); EXPECT_EQ(rootNode.get(), info.layerUpdateQueue->entries().at(0).renderNode.get()); EXPECT_EQ(uirenderer::Rect(0, 0, 200, 400), info.layerUpdateQueue->entries().at(0).damage); 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..3d5aca4bf05a 100644 --- a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp +++ b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp @@ -42,13 +42,16 @@ TEST(SkiaDisplayList, reset) { { SkiaRecordingCanvas canvas{nullptr, 1, 1}; canvas.drawColor(0, SkBlendMode::kSrc); - skiaDL.reset(canvas.finishRecording()); + skiaDL = canvas.finishRecording(); } 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); @@ -81,7 +84,7 @@ TEST(SkiaDisplayList, reuseDisplayList) { // attach a displayList for reuse SkiaDisplayList skiaDL; - ASSERT_TRUE(skiaDL.reuseDisplayList(renderNode.get(), nullptr)); + ASSERT_TRUE(skiaDL.reuseDisplayList(renderNode.get())); // detach the list that you just attempted to reuse availableList = renderNode->detachAvailableList(); @@ -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); @@ -264,10 +263,10 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaDisplayList, prepareListAndChildren_vdOffscr } // Another way to be offscreen: a matrix from the draw call. - for (const SkMatrix translate : { SkMatrix::MakeTrans(width, 0), - SkMatrix::MakeTrans(0, height), - SkMatrix::MakeTrans(-width, 0), - SkMatrix::MakeTrans(0, -height)}) { + for (const SkMatrix translate : { SkMatrix::Translate(width, 0), + SkMatrix::Translate(0, height), + SkMatrix::Translate(-width, 0), + SkMatrix::Translate(0, -height)}) { SkiaDisplayList skiaDL; VectorDrawableRoot dirtyVD(new VectorDrawable::Group()); dirtyVD.mutateProperties()->setBounds(bounds); @@ -292,7 +291,7 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaDisplayList, prepareListAndChildren_vdOffscr SkiaDisplayList skiaDL; VectorDrawableRoot dirtyVD(new VectorDrawable::Group()); dirtyVD.mutateProperties()->setBounds(bounds); - SkMatrix translate = SkMatrix::MakeTrans(50, 50); + SkMatrix translate = SkMatrix::Translate(50, 50); skiaDL.appendVD(&dirtyVD, translate); ASSERT_TRUE(dirtyVD.isDirty()); diff --git a/libs/hwui/tests/unit/SkiaPipelineTests.cpp b/libs/hwui/tests/unit/SkiaPipelineTests.cpp index e7a889d08cfd..6dd57b19a41b 100644 --- a/libs/hwui/tests/unit/SkiaPipelineTests.cpp +++ b/libs/hwui/tests/unit/SkiaPipelineTests.cpp @@ -302,7 +302,8 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaPipeline, clipped) { class ClippedTestCanvas : public SkCanvas { public: ClippedTestCanvas() : SkCanvas(CANVAS_WIDTH, CANVAS_HEIGHT) {} - void onDrawImage(const SkImage*, SkScalar dx, SkScalar dy, const SkPaint*) override { + void onDrawImage2(const SkImage*, SkScalar dx, SkScalar dy, const SkSamplingOptions&, + const SkPaint*) override { EXPECT_EQ(0, mDrawCounter++); EXPECT_EQ(SkRect::MakeLTRB(10, 20, 30, 40), TestUtils::getClipBounds(this)); EXPECT_TRUE(getTotalMatrix().isIdentity()); @@ -336,7 +337,8 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaPipeline, clipped_rotated) { class ClippedTestCanvas : public SkCanvas { public: ClippedTestCanvas() : SkCanvas(CANVAS_WIDTH, CANVAS_HEIGHT) {} - void onDrawImage(const SkImage*, SkScalar dx, SkScalar dy, const SkPaint*) override { + void onDrawImage2(const SkImage*, SkScalar dx, SkScalar dy, const SkSamplingOptions&, + const SkPaint*) override { EXPECT_EQ(0, mDrawCounter++); // Expect clip to be rotated. EXPECT_EQ(SkRect::MakeLTRB(CANVAS_HEIGHT - dirty.fTop - dirty.height(), dirty.fLeft, diff --git a/libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp b/libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp index eec25c6bd40d..15ecf5831f3a 100644 --- a/libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp +++ b/libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp @@ -111,11 +111,11 @@ TEST(RenderNodeDrawable, renderPropTransform) { [](RenderProperties& properties) { properties.setLeftTopRightBottom(10, 10, 110, 110); - SkMatrix staticMatrix = SkMatrix::MakeScale(1.2f, 1.2f); + SkMatrix staticMatrix = SkMatrix::Scale(1.2f, 1.2f); properties.setStaticMatrix(&staticMatrix); // ignored, since static overrides animation - SkMatrix animationMatrix = SkMatrix::MakeTrans(15, 15); + SkMatrix animationMatrix = SkMatrix::Translate(15, 15); properties.setAnimationMatrix(&animationMatrix); properties.setTranslationX(10); 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/services/include/android/os/DropBoxManager.h b/libs/services/include/android/os/DropBoxManager.h index 07472435d8a3..5689286f0b32 100644 --- a/libs/services/include/android/os/DropBoxManager.h +++ b/libs/services/include/android/os/DropBoxManager.h @@ -93,8 +93,6 @@ private: enum { HAS_BYTE_ARRAY = 8 }; - - Status add(const Entry& entry); }; }} // namespace android::os diff --git a/libs/services/src/os/DropBoxManager.cpp b/libs/services/src/os/DropBoxManager.cpp index 429f996bd65e..3716e019f69a 100644 --- a/libs/services/src/os/DropBoxManager.cpp +++ b/libs/services/src/os/DropBoxManager.cpp @@ -18,7 +18,9 @@ #include <android/os/DropBoxManager.h> +#include <android-base/unique_fd.h> #include <binder/IServiceManager.h> +#include <binder/ParcelFileDescriptor.h> #include <com/android/internal/os/IDropBoxManagerService.h> #include <cutils/log.h> @@ -178,18 +180,24 @@ DropBoxManager::~DropBoxManager() Status DropBoxManager::addText(const String16& tag, const string& text) { - Entry entry(tag, IS_TEXT); - entry.mData.assign(text.c_str(), text.c_str() + text.size()); - return add(entry); + return addData(tag, reinterpret_cast<uint8_t const*>(text.c_str()), text.size(), IS_TEXT); } Status DropBoxManager::addData(const String16& tag, uint8_t const* data, size_t size, int flags) { - Entry entry(tag, flags); - entry.mData.assign(data, data+size); - return add(entry); + sp<IDropBoxManagerService> service = interface_cast<IDropBoxManagerService>( + defaultServiceManager()->getService(android::String16("dropbox"))); + if (service == NULL) { + return Status::fromExceptionCode(Status::EX_NULL_POINTER, "can't find dropbox service"); + } + ALOGD("About to call service->add()"); + vector<uint8_t> dataArg; + dataArg.assign(data, data + size); + Status status = service->addData(tag, dataArg, flags); + ALOGD("service->add returned %s", status.toString8().string()); + return status; } Status @@ -213,20 +221,15 @@ DropBoxManager::addFile(const String16& tag, int fd, int flags) ALOGW("DropboxManager: %s", message.c_str()); return Status::fromExceptionCode(Status::EX_ILLEGAL_STATE, message.c_str()); } - Entry entry(tag, flags, fd); - return add(entry); -} - -Status -DropBoxManager::add(const Entry& entry) -{ sp<IDropBoxManagerService> service = interface_cast<IDropBoxManagerService>( defaultServiceManager()->getService(android::String16("dropbox"))); if (service == NULL) { return Status::fromExceptionCode(Status::EX_NULL_POINTER, "can't find dropbox service"); } ALOGD("About to call service->add()"); - Status status = service->add(entry); + android::base::unique_fd uniqueFd(fd); + android::os::ParcelFileDescriptor parcelFd(std::move(uniqueFd)); + Status status = service->addFile(tag, parcelFd, flags); ALOGD("service->add returned %s", status.toString8().string()); return status; } 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> diff --git a/libs/usb/tests/AccessoryChat/src/com/android/accessorychat/AccessoryChat.java b/libs/usb/tests/AccessoryChat/src/com/android/accessorychat/AccessoryChat.java index bf0cef01ac7b..18cfce528205 100644 --- a/libs/usb/tests/AccessoryChat/src/com/android/accessorychat/AccessoryChat.java +++ b/libs/usb/tests/AccessoryChat/src/com/android/accessorychat/AccessoryChat.java @@ -83,7 +83,7 @@ public class AccessoryChat extends Activity implements Runnable, TextView.OnEdit super.onCreate(savedInstanceState); mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE); - mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0); + mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_MUTABLE_UNAUDITED); IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); registerReceiver(mUsbReceiver, filter); |