From 7d958f08e2ad96db6677e0d007d9825f41e7e2fb Mon Sep 17 00:00:00 2001 From: Max Zhang Date: Thu, 8 Dec 2022 17:27:05 +0000 Subject: [2/4] Add user customizable keys (4) for RCU in frameworks/base Define 4 new keys, update the "last key" test case, and assign values in Generic.kl. Project details can be found at go/dipper-custom-button Bug: 269742724 Test: local build Change-Id: If0b12be56fea1a39192da068abdacde8d3e0948f --- data/keyboards/Generic.kl | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'data') diff --git a/data/keyboards/Generic.kl b/data/keyboards/Generic.kl index 0f8616da7359..0961d6e3d275 100644 --- a/data/keyboards/Generic.kl +++ b/data/keyboards/Generic.kl @@ -423,6 +423,10 @@ key 580 APP_SWITCH key 582 VOICE_ASSIST # Linux KEY_ASSISTANT key 583 ASSIST +key 656 MACRO_1 +key 657 MACRO_2 +key 658 MACRO_3 +key 659 MACRO_4 # Keys defined by HID usages key usage 0x0c0067 WINDOW -- cgit v1.2.3-59-g8ed1b From 85ad1b94449fb80d6010390d566299c0e6c43b63 Mon Sep 17 00:00:00 2001 From: Eric Rahm Date: Thu, 23 Mar 2023 16:00:52 +0000 Subject: Add INTERACT_ACROSS_USERS to soundpicker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the `INTERACT_ACROSS_USERS` permission to `android.com.soundpicker`. `SoundPicker` provides a `RingtonePickerActivity` which allows the user to choose a ringtone from all of the available ringtones. To support this is uses a `RingtoneManager` to retrieve a list of all available rintgtones including those of the parent profile. RingtoneManager optionally supports managing ringtones from the parent profile, this is controlled by a boolean flag that `RingtonePickerActivity` sets to true when creating its `RingtoneManager`. When enumerating ringtones one calls `RingtoneManager::getCursor` which builds a set of ringtones that includes “internal ringtones,” “media ringtones,” and optionally parent ringtones if the `RingtoneManager` was configured to included them when created. In this case it is configured to do so and will call `RingtoneManager::getParentRingtones`. This function retrieves the parent profile by calling `UserManager::getProfileParent` and gates following logic on whether or not a `UserInfo` is returned. `UserManager::getProfileParent` requires the caller to have either the `MANAGER_USERS` or `INTERACT_ACROSS_USERS` permission. Without the permission a security exception is thrown similar to the following: ``` 02-01 16:22:29.979 16869 16869 E AndroidRuntime: Caused by: java.lang.SecurityException: You need MANAGE_USERS permission to: get the profile parent 02-01 16:22:29.979 16869 16869 E AndroidRuntime: at android.os.Parcel.createExceptionOrNull(Parcel.java:2373) 02-01 16:22:29.979 16869 16869 E AndroidRuntime: at android.os.Parcel.createException(Parcel.java:2357) 02-01 16:22:29.979 16869 16869 E AndroidRuntime: at android.os.Parcel.readException(Parcel.java:2340) 02-01 16:22:29.979 16869 16869 E AndroidRuntime: at android.os.Parcel.readException(Parcel.java:2282) 02-01 16:22:29.979 16869 16869 E AndroidRuntime: at android.os.IUserManager$Stub$Proxy.getProfileParent(IUserManager.java:2083) 02-01 16:22:29.979 16869 16869 E AndroidRuntime: at android.os.UserManager.getProfileParent(UserManager.java:3564) 02-01 16:22:29.979 16869 16869 E AndroidRuntime: at android.media.RingtoneManager.getParentProfileRingtones(RingtoneManager.java:455) 02-01 16:22:29.979 16869 16869 E AndroidRuntime: at android.media.RingtoneManager.getCursor(RingtoneManager.java:443) 02-01 16:22:29.979 16869 16869 E AndroidRuntime: at com.android.soundpicker.RingtonePickerActivity.initRingtoneManager(RingtonePickerActivity.java:470) 02-01 16:22:29.979 16869 16869 E AndroidRuntime: at com.android.soundpicker.RingtonePickerActivity.onCreate(RingtonePickerActivity.java:188) 02-01 16:22:29.979 16869 16869 E AndroidRuntime: at android.app.Activity.performCreate(Activity.java:7994) 02-01 16:22:29.979 16869 16869 E AndroidRuntime: at android.app.Activity.performCreate(Activity.java:7978) 02-01 16:22:29.979 16869 16869 E AndroidRuntime: at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309) 02-01 16:22:29.979 16869 16869 E AndroidRuntime: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3422) ``` To ameliorate this we add the `INTERACT_ACROSS_USERS` permission to `SoundPicker` to indicate its intent to query the parent profile which matches the intent of `RingtonePickerActivity`. Bug: 178990696 Bug: 246352264 Test: INTERACT_ACROSS_USERS permssion SecurityException is when go to Settings->Watch Ringtone. Co-Authored-by: haiping Change-Id: I448a104cf71c5cb1dbda09091c5db8b558fecdeb (cherry picked from commit 66d2f2afb4cfc531142230cad1af568d1904a7a2) (cherry picked from commit 82dc1c8e464adf01d14ea0e7b1c9064595ad3293) --- data/etc/privapp-permissions-platform.xml | 4 ++++ packages/SoundPicker/AndroidManifest.xml | 2 ++ 2 files changed, 6 insertions(+) (limited to 'data') diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 0faf62e03b14..fe90b4ae3dcf 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -523,6 +523,10 @@ applications that come with the platform + + + + diff --git a/packages/SoundPicker/AndroidManifest.xml b/packages/SoundPicker/AndroidManifest.xml index 6cb885ff807d..cdfe2421fdb7 100644 --- a/packages/SoundPicker/AndroidManifest.xml +++ b/packages/SoundPicker/AndroidManifest.xml @@ -9,6 +9,8 @@ + + Date: Wed, 22 Mar 2023 17:23:29 -0700 Subject: Apply keylayoutfor RCUs with name GoogleTV_Remote bug: 274850387 test: manual Change-Id: Ia4138c5daf76fff4a0afc93a399181515fe7eb90 --- data/keyboards/GoogleTV-Remote.idc | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 data/keyboards/GoogleTV-Remote.idc (limited to 'data') diff --git a/data/keyboards/GoogleTV-Remote.idc b/data/keyboards/GoogleTV-Remote.idc new file mode 100644 index 000000000000..14fb4e29c33c --- /dev/null +++ b/data/keyboards/GoogleTV-Remote.idc @@ -0,0 +1,25 @@ +# Copyright 2023 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Input Device Configuration file for Google Reference Remote Control Unit (RCU). +# +# + +# Basic Parameters +# Depending on the FLASH configurations, RCUs may have PID 0006 instead +# of 0001. +keyboard.layout = Vendor_0957_Product_0001 +keyboard.doNotWakeByDefault = 1 +audio.mic = 1 -- cgit v1.2.3-59-g8ed1b From 798f9f54cba7412eed8674015f5de143ff4e0eab Mon Sep 17 00:00:00 2001 From: Max Zhang Date: Wed, 26 Apr 2023 17:05:39 -0700 Subject: Map MAGIC button HID key to MACRO_1 for Ref RCU Map MAGIC button HID key from Google Reference RCU to Android keycode KEYCODE_MACRO_1 for the MAGIC / customizable button feature. bug: 279825318 test: manual Change-Id: Ic4705f0798ba1630297da83d3bc300afb27d0ad8 --- data/keyboards/Vendor_0957_Product_0001.kl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'data') diff --git a/data/keyboards/Vendor_0957_Product_0001.kl b/data/keyboards/Vendor_0957_Product_0001.kl index 54f8808e2285..5d7fd85fc178 100644 --- a/data/keyboards/Vendor_0957_Product_0001.kl +++ b/data/keyboards/Vendor_0957_Product_0001.kl @@ -45,6 +45,7 @@ key 11 0 # custom keys key usage 0x000c01BB TV_INPUT +key usage 0x000c0186 MACRO_1 key usage 0x000c0185 TV_TELETEXT key usage 0x000c0061 CAPTIONS @@ -77,4 +78,4 @@ key usage 0x000c009D CHANNEL_DOWN key usage 0x000c0077 BUTTON_3 WAKE #YouTube key usage 0x000c0078 BUTTON_4 WAKE #Netflix key usage 0x000c0079 BUTTON_6 WAKE -key usage 0x000c007A BUTTON_7 WAKE \ No newline at end of file +key usage 0x000c007A BUTTON_7 WAKE -- cgit v1.2.3-59-g8ed1b From ab0a8997eabcdd9de4e5a6424f066cfd01775443 Mon Sep 17 00:00:00 2001 From: Caitlin Shkuratov Date: Wed, 19 Apr 2023 21:40:33 +0000 Subject: [Wifi] Add LOCATION_HARDWARE to SysUI permissions. This supports wifi scanning. Fixes: 278310605 Test: compiles Change-Id: I275adeaabb9fcda27ed66ab05442b48d4ac4613f --- data/etc/com.android.systemui.xml | 1 + packages/SystemUI/AndroidManifest.xml | 1 + 2 files changed, 2 insertions(+) (limited to 'data') diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml index 922dbb5fef38..43683ffad432 100644 --- a/data/etc/com.android.systemui.xml +++ b/data/etc/com.android.systemui.xml @@ -31,6 +31,7 @@ + diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 6e55000dfe8a..4c56582ce89e 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -82,6 +82,7 @@ + -- cgit v1.2.3-59-g8ed1b From 676e16928f664899d12ec5aa85d48a1b0f563caf Mon Sep 17 00:00:00 2001 From: Raphael Kim Date: Thu, 20 Apr 2023 05:44:35 -0700 Subject: Add USE_ATTESTATION_VERIFICATION_SERVICE permission to shell for GTS test Bug: 276367261 Test: gts-tf > run gts-dev -m GtsAttestationVerificationDeviceSideTestCases (with local changes to turn off boot state check) Change-Id: I18d2f77291d0b6ae7f12acecb5b786c9d3657abf --- data/etc/privapp-permissions-platform.xml | 2 ++ packages/Shell/AndroidManifest.xml | 3 +++ 2 files changed, 5 insertions(+) (limited to 'data') diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index addd17b85845..bee77977577e 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -520,6 +520,8 @@ applications that come with the platform + + diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index c01518fcdfc4..7d112f998826 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -837,6 +837,9 @@ + + + Date: Fri, 28 Apr 2023 16:42:56 -0700 Subject: Add android.permission.LAUNCH_CREDENTIAL_SELECTOR which is needed for GtsCredentialsTestCases Bug: 277637274 Test: atest GtsCredentialsTestCases (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:07211d002288eafb9689298570282c51c9c7631c) cherrypick to resolve merging conflict Change-Id: I2b805541a998160313724d24ac1ba9a826fd1d25 --- data/etc/privapp-permissions-platform.xml | 2 + packages/Shell/AndroidManifest.xml | 120 ++++++++++++++++-------------- 2 files changed, 65 insertions(+), 57 deletions(-) (limited to 'data') diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index bee77977577e..00cb81004ca8 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -522,6 +522,8 @@ applications that come with the platform + + diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 7d112f998826..15157983e537 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -125,7 +125,7 @@ - + @@ -136,7 +136,7 @@ - + @@ -231,16 +231,16 @@ - - - + + + - + @@ -304,7 +304,7 @@ - + @@ -344,7 +344,7 @@ - + @@ -386,54 +386,54 @@ - + - + - - - - + + + + - + - - + + - + - + - + - + - + - + - + - + - - + + @@ -442,8 +442,8 @@ - - + + @@ -470,7 +470,7 @@ - + @@ -495,7 +495,7 @@ - + @@ -560,14 +560,14 @@ - + - - + + @@ -630,21 +630,21 @@ - + - + - + @@ -820,8 +820,8 @@ - - + + @@ -832,7 +832,7 @@ - + @@ -840,10 +840,14 @@ - + + + + - + - + + android:permission="android.permission.TRIGGER_SHELL_PROFCOLLECT_UPLOAD"> @@ -918,6 +924,6 @@ + android:exported="false" /> -- cgit v1.2.3-59-g8ed1b From f6a1d4baa9d5106ee1925139976d8c338917f52d Mon Sep 17 00:00:00 2001 From: Seigo Nonaka Date: Mon, 29 May 2023 17:49:43 +0900 Subject: Add RobotoFlex into AOSP system image Cherry picked I8324c16fa8c392163e81fb9f4c21bba3343b6df0 to master due to b/286408867. Code was merged via `-s ours` and is missing even though the sha exists on the target branch. Cherry pick is required to bring in the code. Manually remove the "Merged in" directive to allow for downstream propagation. (cherry picked from https://android-review.googlesource.com/q/commit:2872d3fa5b6923dbcfea99c4e5e7fd6bb3526f5f) Bug: 235303866 Test: Manually done Change-Id: I587161bd6fca5ac02a127a675ceab4f9203d2652 --- data/fonts/fonts.xml | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) (limited to 'data') diff --git a/data/fonts/fonts.xml b/data/fonts/fonts.xml index 0563519fe419..4a780bc2dd22 100644 --- a/data/fonts/fonts.xml +++ b/data/fonts/fonts.xml @@ -277,6 +277,99 @@ + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + + -- cgit v1.2.3-59-g8ed1b From a7967b83cd1eb6faa33a7e95802de717a872c66f Mon Sep 17 00:00:00 2001 From: Sergej Salnikov Date: Fri, 16 Jun 2023 15:13:59 +0000 Subject: Wake the device on MACRO_1 key event Bug: 279825318 Change-Id: Ic6b4a812098315b3ce6c6ac13c22e43d57e902d5 --- data/keyboards/Vendor_0957_Product_0001.kl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'data') diff --git a/data/keyboards/Vendor_0957_Product_0001.kl b/data/keyboards/Vendor_0957_Product_0001.kl index 5d7fd85fc178..1da3ba6efc2b 100644 --- a/data/keyboards/Vendor_0957_Product_0001.kl +++ b/data/keyboards/Vendor_0957_Product_0001.kl @@ -45,7 +45,7 @@ key 11 0 # custom keys key usage 0x000c01BB TV_INPUT -key usage 0x000c0186 MACRO_1 +key usage 0x000c0186 MACRO_1 WAKE key usage 0x000c0185 TV_TELETEXT key usage 0x000c0061 CAPTIONS -- cgit v1.2.3-59-g8ed1b From c235a386cc1713a59df78a2253da432f146c0587 Mon Sep 17 00:00:00 2001 From: Seigo Nonaka Date: Mon, 3 Jul 2023 14:25:26 +0900 Subject: Deprecate fonts.xml and add hidden font_fallback.xml Not to break the apps that reads fonts.xml and add new features to the system font customization at the same time, this CL introduces new hidden XML file font_fallback.xml which is currently a copy of the fonts.xml. The fonts.xml still exists but may not contain the latest system font settings due to compatibility problems. Application should use the public API for accessing system installed fonts which is available from API29. Bug: 281769620 Test: atest CtsGraphicsTestCases Test: atest CtsTextTestCases Change-Id: I556b5fffb0c78f0c6150b472fd240b9a546de93f --- data/fonts/Android.bp | 5 + data/fonts/font_fallback.xml | 1630 ++++++++++++++++++++ data/fonts/fonts.mk | 1 + data/fonts/fonts.xml | 12 + .../java/android/graphics/fonts/SystemFonts.java | 2 +- 5 files changed, 1649 insertions(+), 1 deletion(-) create mode 100644 data/fonts/font_fallback.xml (limited to 'data') diff --git a/data/fonts/Android.bp b/data/fonts/Android.bp index f90a74d939f4..3dd9ba9db1d9 100644 --- a/data/fonts/Android.bp +++ b/data/fonts/Android.bp @@ -52,3 +52,8 @@ prebuilt_etc { name: "fonts.xml", src: "fonts.xml", } + +prebuilt_etc { + name: "font_fallback.xml", + src: "font_fallback.xml", +} diff --git a/data/fonts/font_fallback.xml b/data/fonts/font_fallback.xml new file mode 100644 index 000000000000..1e97fcee250d --- /dev/null +++ b/data/fonts/font_fallback.xml @@ -0,0 +1,1630 @@ + + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + + + + + + + + + + + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + Roboto-Regular.ttf + + + + + + + + + + NotoSerif-Regular.ttf + NotoSerif-Bold.ttf + NotoSerif-Italic.ttf + NotoSerif-BoldItalic.ttf + + + + + + + + + + + + + DroidSansMono.ttf + + + + + + CutiveMono.ttf + + + + + + ComingSoon.ttf + + + + DancingScript-Regular.ttf + + + DancingScript-Regular.ttf + + + + + + CarroisGothicSC-Regular.ttf + + + + SourceSansPro-Regular.ttf + SourceSansPro-Italic.ttf + SourceSansPro-SemiBold.ttf + SourceSansPro-SemiBoldItalic.ttf + SourceSansPro-Bold.ttf + SourceSansPro-BoldItalic.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + RobotoFlex-Regular.ttf + + + + + + + + + + NotoNaskhArabic-Regular.ttf + + NotoNaskhArabic-Bold.ttf + + + + NotoNaskhArabicUI-Regular.ttf + + NotoNaskhArabicUI-Bold.ttf + + + + NotoSansEthiopic-VF.ttf + + + + NotoSansEthiopic-VF.ttf + + + + NotoSansEthiopic-VF.ttf + + + + NotoSansEthiopic-VF.ttf + + + NotoSerifEthiopic-VF.ttf + + + NotoSerifEthiopic-VF.ttf + + + NotoSerifEthiopic-VF.ttf + + + NotoSerifEthiopic-VF.ttf + + + + + + NotoSansHebrew-Regular.ttf + + NotoSansHebrew-Bold.ttf + NotoSerifHebrew-Regular.ttf + NotoSerifHebrew-Bold.ttf + + + NotoSansThai-Regular.ttf + + NotoSansThai-Bold.ttf + + NotoSerifThai-Regular.ttf + + NotoSerifThai-Bold.ttf + + + + NotoSansThaiUI-Regular.ttf + + NotoSansThaiUI-Bold.ttf + + + + NotoSansArmenian-VF.ttf + + + + NotoSansArmenian-VF.ttf + + + + NotoSansArmenian-VF.ttf + + + + NotoSansArmenian-VF.ttf + + + NotoSerifArmenian-VF.ttf + + + NotoSerifArmenian-VF.ttf + + + NotoSerifArmenian-VF.ttf + + + NotoSerifArmenian-VF.ttf + + + + + + NotoSansGeorgian-VF.ttf + + + + NotoSansGeorgian-VF.ttf + + + + NotoSansGeorgian-VF.ttf + + + + NotoSansGeorgian-VF.ttf + + + NotoSerifGeorgian-VF.ttf + + + NotoSerifGeorgian-VF.ttf + + + NotoSerifGeorgian-VF.ttf + + + NotoSerifGeorgian-VF.ttf + + + + + + NotoSansDevanagari-VF.ttf + + + + NotoSansDevanagari-VF.ttf + + + + NotoSansDevanagari-VF.ttf + + + + NotoSansDevanagari-VF.ttf + + + NotoSerifDevanagari-VF.ttf + + + NotoSerifDevanagari-VF.ttf + + + NotoSerifDevanagari-VF.ttf + + + NotoSerifDevanagari-VF.ttf + + + + + + NotoSansDevanagariUI-VF.ttf + + + + NotoSansDevanagariUI-VF.ttf + + + + NotoSansDevanagariUI-VF.ttf + + + + NotoSansDevanagariUI-VF.ttf + + + + + + + + NotoSansGujarati-Regular.ttf + + NotoSansGujarati-Bold.ttf + NotoSerifGujarati-VF.ttf + + + NotoSerifGujarati-VF.ttf + + + NotoSerifGujarati-VF.ttf + + + NotoSerifGujarati-VF.ttf + + + + + + NotoSansGujaratiUI-Regular.ttf + + NotoSansGujaratiUI-Bold.ttf + + + + NotoSansGurmukhi-VF.ttf + + + + NotoSansGurmukhi-VF.ttf + + + + NotoSansGurmukhi-VF.ttf + + + + NotoSansGurmukhi-VF.ttf + + + NotoSerifGurmukhi-VF.ttf + + + NotoSerifGurmukhi-VF.ttf + + + NotoSerifGurmukhi-VF.ttf + + + NotoSerifGurmukhi-VF.ttf + + + + + + NotoSansGurmukhiUI-VF.ttf + + + + NotoSansGurmukhiUI-VF.ttf + + + + NotoSansGurmukhiUI-VF.ttf + + + + NotoSansGurmukhiUI-VF.ttf + + + + + + NotoSansTamil-VF.ttf + + + + NotoSansTamil-VF.ttf + + + + NotoSansTamil-VF.ttf + + + + NotoSansTamil-VF.ttf + + + NotoSerifTamil-VF.ttf + + + NotoSerifTamil-VF.ttf + + + NotoSerifTamil-VF.ttf + + + NotoSerifTamil-VF.ttf + + + + + + NotoSansTamilUI-VF.ttf + + + + NotoSansTamilUI-VF.ttf + + + + NotoSansTamilUI-VF.ttf + + + + NotoSansTamilUI-VF.ttf + + + + + + NotoSansMalayalam-VF.ttf + + + + NotoSansMalayalam-VF.ttf + + + + NotoSansMalayalam-VF.ttf + + + + NotoSansMalayalam-VF.ttf + + + NotoSerifMalayalam-VF.ttf + + + NotoSerifMalayalam-VF.ttf + + + NotoSerifMalayalam-VF.ttf + + + NotoSerifMalayalam-VF.ttf + + + + + + NotoSansMalayalamUI-VF.ttf + + + + NotoSansMalayalamUI-VF.ttf + + + + NotoSansMalayalamUI-VF.ttf + + + + NotoSansMalayalamUI-VF.ttf + + + + + + NotoSansBengali-VF.ttf + + + + NotoSansBengali-VF.ttf + + + + NotoSansBengali-VF.ttf + + + + NotoSansBengali-VF.ttf + + + NotoSerifBengali-VF.ttf + + + NotoSerifBengali-VF.ttf + + + NotoSerifBengali-VF.ttf + + + NotoSerifBengali-VF.ttf + + + + + + NotoSansBengaliUI-VF.ttf + + + + NotoSansBengaliUI-VF.ttf + + + + NotoSansBengaliUI-VF.ttf + + + + NotoSansBengaliUI-VF.ttf + + + + + + NotoSansTelugu-VF.ttf + + + + NotoSansTelugu-VF.ttf + + + + NotoSansTelugu-VF.ttf + + + + NotoSansTelugu-VF.ttf + + + NotoSerifTelugu-VF.ttf + + + NotoSerifTelugu-VF.ttf + + + NotoSerifTelugu-VF.ttf + + + NotoSerifTelugu-VF.ttf + + + + + + NotoSansTeluguUI-VF.ttf + + + + NotoSansTeluguUI-VF.ttf + + + + NotoSansTeluguUI-VF.ttf + + + + NotoSansTeluguUI-VF.ttf + + + + + + NotoSansKannada-VF.ttf + + + + NotoSansKannada-VF.ttf + + + + NotoSansKannada-VF.ttf + + + + NotoSansKannada-VF.ttf + + + NotoSerifKannada-VF.ttf + + + NotoSerifKannada-VF.ttf + + + NotoSerifKannada-VF.ttf + + + NotoSerifKannada-VF.ttf + + + + + + NotoSansKannadaUI-VF.ttf + + + + NotoSansKannadaUI-VF.ttf + + + + NotoSansKannadaUI-VF.ttf + + + + NotoSansKannadaUI-VF.ttf + + + + + NotoSansOriya-Regular.ttf + + NotoSansOriya-Bold.ttf + + + + NotoSansOriyaUI-Regular.ttf + + NotoSansOriyaUI-Bold.ttf + + + + NotoSansSinhala-VF.ttf + + + + NotoSansSinhala-VF.ttf + + + + NotoSansSinhala-VF.ttf + + + + NotoSansSinhala-VF.ttf + + + NotoSerifSinhala-VF.ttf + + + NotoSerifSinhala-VF.ttf + + + NotoSerifSinhala-VF.ttf + + + NotoSerifSinhala-VF.ttf + + + + + + NotoSansSinhalaUI-VF.ttf + + + + NotoSansSinhalaUI-VF.ttf + + + + NotoSansSinhalaUI-VF.ttf + + + + NotoSansSinhalaUI-VF.ttf + + + + + + NotoSansKhmer-VF.ttf + + + + + NotoSansKhmer-VF.ttf + + + + + NotoSansKhmer-VF.ttf + + + + + NotoSansKhmer-VF.ttf + + + + + NotoSansKhmer-VF.ttf + + + + + NotoSansKhmer-VF.ttf + + + + + NotoSansKhmer-VF.ttf + + + + + NotoSansKhmer-VF.ttf + + + + + NotoSansKhmer-VF.ttf + + + + NotoSerifKhmer-Regular.otf + NotoSerifKhmer-Bold.otf + + + + NotoSansKhmerUI-Regular.ttf + + NotoSansKhmerUI-Bold.ttf + + + NotoSansLao-Regular.ttf + + NotoSansLao-Bold.ttf + + NotoSerifLao-Regular.ttf + + NotoSerifLao-Bold.ttf + + + NotoSansLaoUI-Regular.ttf + + NotoSansLaoUI-Bold.ttf + + + NotoSansMyanmar-Regular.otf + NotoSansMyanmar-Medium.otf + NotoSansMyanmar-Bold.otf + NotoSerifMyanmar-Regular.otf + NotoSerifMyanmar-Bold.otf + + + NotoSansMyanmarUI-Regular.otf + NotoSansMyanmarUI-Medium.otf + NotoSansMyanmarUI-Bold.otf + + + + NotoSansThaana-Regular.ttf + + NotoSansThaana-Bold.ttf + + + NotoSansCham-Regular.ttf + + NotoSansCham-Bold.ttf + + + NotoSansAhom-Regular.otf + + + + NotoSansAdlam-VF.ttf + + + + NotoSansAdlam-VF.ttf + + + + NotoSansAdlam-VF.ttf + + + + NotoSansAdlam-VF.ttf + + + + + + NotoSansAvestan-Regular.ttf + + + + + NotoSansBalinese-Regular.ttf + + + + NotoSansBamum-Regular.ttf + + + + NotoSansBatak-Regular.ttf + + + + + NotoSansBrahmi-Regular.ttf + + + + + NotoSansBuginese-Regular.ttf + + + + NotoSansBuhid-Regular.ttf + + + + + NotoSansCanadianAboriginal-Regular.ttf + + + + + NotoSansCarian-Regular.ttf + + + + NotoSansChakma-Regular.otf + + + NotoSansCherokee-Regular.ttf + + + + NotoSansCoptic-Regular.ttf + + + + + NotoSansCuneiform-Regular.ttf + + + + + NotoSansCypriot-Regular.ttf + + + + + NotoSansDeseret-Regular.ttf + + + + + NotoSansEgyptianHieroglyphs-Regular.ttf + + + + NotoSansElbasan-Regular.otf + + + + NotoSansGlagolitic-Regular.ttf + + + + + NotoSansGothic-Regular.ttf + + + + + NotoSansHanunoo-Regular.ttf + + + + + NotoSansImperialAramaic-Regular.ttf + + + + + NotoSansInscriptionalPahlavi-Regular.ttf + + + + + NotoSansInscriptionalParthian-Regular.ttf + + + + NotoSansJavanese-Regular.otf + + + + NotoSansKaithi-Regular.ttf + + + + + NotoSansKayahLi-Regular.ttf + + + + + NotoSansKharoshthi-Regular.ttf + + + + + NotoSansLepcha-Regular.ttf + + + + NotoSansLimbu-Regular.ttf + + + + + NotoSansLinearB-Regular.ttf + + + + NotoSansLisu-Regular.ttf + + + + + NotoSansLycian-Regular.ttf + + + + + NotoSansLydian-Regular.ttf + + + + + NotoSansMandaic-Regular.ttf + + + + + NotoSansMeeteiMayek-Regular.ttf + + + + + NotoSansNewTaiLue-Regular.ttf + + + + NotoSansNKo-Regular.ttf + + + + NotoSansOgham-Regular.ttf + + + + + NotoSansOlChiki-Regular.ttf + + + + + NotoSansOldItalic-Regular.ttf + + + + + NotoSansOldPersian-Regular.ttf + + + + + NotoSansOldSouthArabian-Regular.ttf + + + + + NotoSansOldTurkic-Regular.ttf + + + + NotoSansOsage-Regular.ttf + + + + NotoSansOsmanya-Regular.ttf + + + + + NotoSansPhoenician-Regular.ttf + + + + + NotoSansRejang-Regular.ttf + + + + NotoSansRunic-Regular.ttf + + + + + NotoSansSamaritan-Regular.ttf + + + + + NotoSansSaurashtra-Regular.ttf + + + + + NotoSansShavian-Regular.ttf + + + + + NotoSansSundanese-Regular.ttf + + + + + NotoSansSylotiNagri-Regular.ttf + + + + + + NotoSansSyriacEstrangela-Regular.ttf + + + + + NotoSansSyriacEastern-Regular.ttf + + + + + NotoSansSyriacWestern-Regular.ttf + + + + + NotoSansTagalog-Regular.ttf + + + + + NotoSansTagbanwa-Regular.ttf + + + + + NotoSansTaiTham-Regular.ttf + + + + + NotoSansTaiViet-Regular.ttf + + + + + NotoSerifTibetan-VF.ttf + + + + NotoSerifTibetan-VF.ttf + + + + NotoSerifTibetan-VF.ttf + + + + NotoSerifTibetan-VF.ttf + + + + + NotoSansTifinagh-Regular.otf + + + + NotoSansUgaritic-Regular.ttf + + + + NotoSansVai-Regular.ttf + + + + NotoSansSymbols-Regular-Subsetted.ttf + + + + NotoSansCJK-Regular.ttc + + NotoSerifCJK-Regular.ttc + + + + + NotoSansCJK-Regular.ttc + + NotoSerifCJK-Regular.ttc + + + + + NotoSansCJK-Regular.ttc + + NotoSerifCJK-Regular.ttc + + + + + NotoSansCJK-Regular.ttc + + NotoSerifCJK-Regular.ttc + + + + NotoColorEmoji.ttf + + + NotoColorEmojiFlags.ttf + + + NotoSansSymbols-Regular-Subsetted2.ttf + + + + NotoSansTaiLe-Regular.ttf + + + + NotoSansYi-Regular.ttf + + + + NotoSansMongolian-Regular.ttf + + + + + NotoSansPhagsPa-Regular.ttf + + + + NotoSansAnatolianHieroglyphs-Regular.otf + + + NotoSansBassaVah-Regular.otf + + + NotoSansBhaiksuki-Regular.otf + + + NotoSansHatran-Regular.otf + + + NotoSansLinearA-Regular.otf + + + NotoSansManichaean-Regular.otf + + + NotoSansMarchen-Regular.otf + + + NotoSansMeroitic-Regular.otf + + + NotoSansMiao-Regular.otf + + + NotoSansMro-Regular.otf + + + NotoSansMultani-Regular.otf + + + NotoSansNabataean-Regular.otf + + + NotoSansNewa-Regular.otf + + + NotoSansOldNorthArabian-Regular.otf + + + NotoSansOldPermic-Regular.otf + + + NotoSansPahawhHmong-Regular.otf + + + NotoSansPalmyrene-Regular.otf + + + NotoSansPauCinHau-Regular.otf + + + NotoSansSharada-Regular.otf + + + NotoSansSoraSompeng-Regular.otf + + + NotoSansGunjalaGondi-Regular.otf + + + NotoSansHanifiRohingya-Regular.otf + + + NotoSansKhojki-Regular.otf + + + NotoSansMasaramGondi-Regular.otf + + + NotoSansWancho-Regular.otf + + + NotoSansWarangCiti-Regular.otf + + + NotoSansGrantha-Regular.ttf + + + NotoSansModi-Regular.ttf + + + NotoSerifDogra-Regular.ttf + + + + NotoSansMedefaidrin-VF.ttf + + + + NotoSansMedefaidrin-VF.ttf + + + + NotoSansMedefaidrin-VF.ttf + + + + NotoSansMedefaidrin-VF.ttf + + + + + + NotoSansSoyombo-VF.ttf + + + + NotoSansSoyombo-VF.ttf + + + + NotoSansSoyombo-VF.ttf + + + + NotoSansSoyombo-VF.ttf + + + + + + NotoSansTakri-VF.ttf + + + + NotoSansTakri-VF.ttf + + + + NotoSansTakri-VF.ttf + + + + NotoSansTakri-VF.ttf + + + + + + NotoSerifNyiakengPuachueHmong-VF.ttf + + + + NotoSerifNyiakengPuachueHmong-VF.ttf + + + + NotoSerifNyiakengPuachueHmong-VF.ttf + + + + NotoSerifNyiakengPuachueHmong-VF.ttf + + + + + + NotoSerifYezidi-VF.ttf + + + + NotoSerifYezidi-VF.ttf + + + + NotoSerifYezidi-VF.ttf + + + + NotoSerifYezidi-VF.ttf + + + + diff --git a/data/fonts/fonts.mk b/data/fonts/fonts.mk index e884f2fe4bbb..5d1cc666efa4 100644 --- a/data/fonts/fonts.mk +++ b/data/fonts/fonts.mk @@ -17,4 +17,5 @@ PRODUCT_PACKAGES := \ DroidSansMono.ttf \ AndroidClock.ttf \ + font_fallback.xml \ fonts.xml diff --git a/data/fonts/fonts.xml b/data/fonts/fonts.xml index 4a780bc2dd22..9320c144d2ec 100644 --- a/data/fonts/fonts.xml +++ b/data/fonts/fonts.xml @@ -1,5 +1,17 @@ + + diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 591e505a792f..fac6aac1db16 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1444,6 +1444,11 @@ This app can receive callbacks when any camera device is being opened (by what application) or closed. + + Allow an application or service to access camera as Headless System User. + + This app can access camera as Headless System User. + control vibration diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 3206dd2123d5..69aa40156e78 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -235,6 +235,7 @@ applications that come with the platform + diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 323f65f4f066..9c93e3601edf 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -23,6 +23,7 @@ > + -- cgit v1.2.3-59-g8ed1b From 7f6d2ed4976765c42fe75536011b19bd3f1ecc98 Mon Sep 17 00:00:00 2001 From: Ikram Gabiyev Date: Tue, 29 Aug 2023 15:22:31 -0700 Subject: Implement btn nav auto enter pip2 flow 1/3 Implement most changes in Core to support the btn nav + auto-enter pip flow for pip2 experiment. Design of this flow is described in more detail here: go/pip2-transitions. The change should not have any effects when pip2 experiment is off. Bug: 298263450 Test: mp droid Change-Id: Iba32999a55238a0e5e00df37ec9a2ba4db8d7996 --- .../java/android/window/TransitionRequestInfo.java | 70 ++++++++++++++++++---- data/etc/services.core.protolog.json | 18 ++---- .../java/com/android/server/wm/ActivityRecord.java | 12 +++- .../server/wm/ActivityTaskManagerService.java | 28 +++++++++ services/core/java/com/android/server/wm/Task.java | 9 +-- .../java/com/android/server/wm/TaskFragment.java | 19 +++++- .../java/com/android/server/wm/Transition.java | 17 ++++++ .../android/server/wm/TransitionController.java | 18 ++++-- 8 files changed, 155 insertions(+), 36 deletions(-) (limited to 'data') diff --git a/core/java/android/window/TransitionRequestInfo.java b/core/java/android/window/TransitionRequestInfo.java index edea2978c340..9b10a7ff5d12 100644 --- a/core/java/android/window/TransitionRequestInfo.java +++ b/core/java/android/window/TransitionRequestInfo.java @@ -36,11 +36,17 @@ public final class TransitionRequestInfo implements Parcelable { private final @WindowManager.TransitionType int mType; /** - * If non-null, If non-null, the task containing the activity whose lifecycle change (start or + * If non-null, the task containing the activity whose lifecycle change (start or * finish) has caused this transition to occur. */ private @Nullable ActivityManager.RunningTaskInfo mTriggerTask; + /** + * If non-null, the task containing the pip activity that participates in this + * transition. + */ + private @Nullable ActivityManager.RunningTaskInfo mPipTask; + /** If non-null, a remote-transition associated with the source of this transition. */ private @Nullable RemoteTransition mRemoteTransition; @@ -59,7 +65,8 @@ public final class TransitionRequestInfo implements Parcelable { @WindowManager.TransitionType int type, @Nullable ActivityManager.RunningTaskInfo triggerTask, @Nullable RemoteTransition remoteTransition) { - this(type, triggerTask, remoteTransition, null /* displayChange */, 0 /* flags */); + this(type, triggerTask, null /* pipTask */, + remoteTransition, null /* displayChange */, 0 /* flags */); } /** constructor override */ @@ -68,7 +75,17 @@ public final class TransitionRequestInfo implements Parcelable { @Nullable ActivityManager.RunningTaskInfo triggerTask, @Nullable RemoteTransition remoteTransition, int flags) { - this(type, triggerTask, remoteTransition, null /* displayChange */, flags); + this(type, triggerTask, null /* pipTask */, + remoteTransition, null /* displayChange */, flags); + } + + public TransitionRequestInfo( + @WindowManager.TransitionType int type, + @Nullable ActivityManager.RunningTaskInfo triggerTask, + @Nullable RemoteTransition remoteTransition, + @Nullable TransitionRequestInfo.DisplayChange displayChange, + int flags) { + this(type, triggerTask, null /* pipTask */, remoteTransition, displayChange, flags); } /** Requested change to a display. */ @@ -246,7 +263,7 @@ public final class TransitionRequestInfo implements Parcelable { }; @DataClass.Generated( - time = 1691627678294L, + time = 1693425051905L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/window/TransitionRequestInfo.java", inputSignatures = "private final int mDisplayId\nprivate @android.annotation.Nullable android.graphics.Rect mStartAbsBounds\nprivate @android.annotation.Nullable android.graphics.Rect mEndAbsBounds\nprivate int mStartRotation\nprivate int mEndRotation\nprivate boolean mPhysicalDisplayChanged\nclass DisplayChange extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genSetters=true, genBuilder=false, genConstructor=false)") @@ -283,6 +300,9 @@ public final class TransitionRequestInfo implements Parcelable { * @param triggerTask * If non-null, If non-null, the task containing the activity whose lifecycle change (start or * finish) has caused this transition to occur. + * @param pipTask + * If non-null, If non-null, the task containing the activity whose lifecycle change (start or + * finish) has caused this transition to occur. * @param remoteTransition * If non-null, a remote-transition associated with the source of this transition. * @param displayChange @@ -296,6 +316,7 @@ public final class TransitionRequestInfo implements Parcelable { public TransitionRequestInfo( @WindowManager.TransitionType int type, @Nullable ActivityManager.RunningTaskInfo triggerTask, + @Nullable ActivityManager.RunningTaskInfo pipTask, @Nullable RemoteTransition remoteTransition, @Nullable TransitionRequestInfo.DisplayChange displayChange, int flags) { @@ -303,6 +324,7 @@ public final class TransitionRequestInfo implements Parcelable { com.android.internal.util.AnnotationValidations.validate( WindowManager.TransitionType.class, null, mType); this.mTriggerTask = triggerTask; + this.mPipTask = pipTask; this.mRemoteTransition = remoteTransition; this.mDisplayChange = displayChange; this.mFlags = flags; @@ -319,7 +341,7 @@ public final class TransitionRequestInfo implements Parcelable { } /** - * If non-null, If non-null, the task containing the activity whose lifecycle change (start or + * If non-null, the task containing the activity whose lifecycle change (start or * finish) has caused this transition to occur. */ @DataClass.Generated.Member @@ -327,6 +349,15 @@ public final class TransitionRequestInfo implements Parcelable { return mTriggerTask; } + /** + * If non-null, the task containing the pip activity that participates in this + * transition. + */ + @DataClass.Generated.Member + public @Nullable ActivityManager.RunningTaskInfo getPipTask() { + return mPipTask; + } + /** * If non-null, a remote-transition associated with the source of this transition. */ @@ -354,7 +385,7 @@ public final class TransitionRequestInfo implements Parcelable { } /** - * If non-null, If non-null, the task containing the activity whose lifecycle change (start or + * If non-null, the task containing the activity whose lifecycle change (start or * finish) has caused this transition to occur. */ @DataClass.Generated.Member @@ -363,6 +394,16 @@ public final class TransitionRequestInfo implements Parcelable { return this; } + /** + * If non-null, the task containing the pip activity that participates in this + * transition. + */ + @DataClass.Generated.Member + public @android.annotation.NonNull TransitionRequestInfo setPipTask(@android.annotation.NonNull ActivityManager.RunningTaskInfo value) { + mPipTask = value; + return this; + } + /** * If non-null, a remote-transition associated with the source of this transition. */ @@ -392,6 +433,7 @@ public final class TransitionRequestInfo implements Parcelable { return "TransitionRequestInfo { " + "type = " + mType + ", " + "triggerTask = " + mTriggerTask + ", " + + "pipTask = " + mPipTask + ", " + "remoteTransition = " + mRemoteTransition + ", " + "displayChange = " + mDisplayChange + ", " + "flags = " + mFlags + @@ -406,11 +448,13 @@ public final class TransitionRequestInfo implements Parcelable { byte flg = 0; if (mTriggerTask != null) flg |= 0x2; - if (mRemoteTransition != null) flg |= 0x4; - if (mDisplayChange != null) flg |= 0x8; + if (mPipTask != null) flg |= 0x4; + if (mRemoteTransition != null) flg |= 0x8; + if (mDisplayChange != null) flg |= 0x10; dest.writeByte(flg); dest.writeInt(mType); if (mTriggerTask != null) dest.writeTypedObject(mTriggerTask, flags); + if (mPipTask != null) dest.writeTypedObject(mPipTask, flags); if (mRemoteTransition != null) dest.writeTypedObject(mRemoteTransition, flags); if (mDisplayChange != null) dest.writeTypedObject(mDisplayChange, flags); dest.writeInt(mFlags); @@ -430,14 +474,16 @@ public final class TransitionRequestInfo implements Parcelable { byte flg = in.readByte(); int type = in.readInt(); ActivityManager.RunningTaskInfo triggerTask = (flg & 0x2) == 0 ? null : (ActivityManager.RunningTaskInfo) in.readTypedObject(ActivityManager.RunningTaskInfo.CREATOR); - RemoteTransition remoteTransition = (flg & 0x4) == 0 ? null : (RemoteTransition) in.readTypedObject(RemoteTransition.CREATOR); - TransitionRequestInfo.DisplayChange displayChange = (flg & 0x8) == 0 ? null : (TransitionRequestInfo.DisplayChange) in.readTypedObject(TransitionRequestInfo.DisplayChange.CREATOR); + ActivityManager.RunningTaskInfo pipTask = (flg & 0x4) == 0 ? null : (ActivityManager.RunningTaskInfo) in.readTypedObject(ActivityManager.RunningTaskInfo.CREATOR); + RemoteTransition remoteTransition = (flg & 0x8) == 0 ? null : (RemoteTransition) in.readTypedObject(RemoteTransition.CREATOR); + TransitionRequestInfo.DisplayChange displayChange = (flg & 0x10) == 0 ? null : (TransitionRequestInfo.DisplayChange) in.readTypedObject(TransitionRequestInfo.DisplayChange.CREATOR); int flags = in.readInt(); this.mType = type; com.android.internal.util.AnnotationValidations.validate( WindowManager.TransitionType.class, null, mType); this.mTriggerTask = triggerTask; + this.mPipTask = pipTask; this.mRemoteTransition = remoteTransition; this.mDisplayChange = displayChange; this.mFlags = flags; @@ -460,10 +506,10 @@ public final class TransitionRequestInfo implements Parcelable { }; @DataClass.Generated( - time = 1691627678327L, + time = 1693425051928L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/window/TransitionRequestInfo.java", - inputSignatures = "private final @android.view.WindowManager.TransitionType int mType\nprivate @android.annotation.Nullable android.app.ActivityManager.RunningTaskInfo mTriggerTask\nprivate @android.annotation.Nullable android.window.RemoteTransition mRemoteTransition\nprivate @android.annotation.Nullable android.window.TransitionRequestInfo.DisplayChange mDisplayChange\nprivate final int mFlags\nclass TransitionRequestInfo extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genSetters=true, genAidl=true)") + inputSignatures = "private final @android.view.WindowManager.TransitionType int mType\nprivate @android.annotation.Nullable android.app.ActivityManager.RunningTaskInfo mTriggerTask\nprivate @android.annotation.Nullable android.app.ActivityManager.RunningTaskInfo mPipTask\nprivate @android.annotation.Nullable android.window.RemoteTransition mRemoteTransition\nprivate @android.annotation.Nullable android.window.TransitionRequestInfo.DisplayChange mDisplayChange\nprivate final int mFlags\nclass TransitionRequestInfo extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genSetters=true, genAidl=true)") @Deprecated private void __metadata() {} diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 6057852a7e4b..f72cc1d9f3d2 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -2401,6 +2401,12 @@ "group": "WM_DEBUG_TASKS", "at": "com\/android\/server\/wm\/RootWindowContainer.java" }, + "61363198": { + "message": "Auto-PIP allowed, requesting PIP mode via requestStartTransition(): %s, willAutoPip: %b", + "level": "DEBUG", + "group": "WM_DEBUG_STATES", + "at": "com\/android\/server\/wm\/TaskFragment.java" + }, "74885950": { "message": "Waiting for top state to be released by %s", "level": "VERBOSE", @@ -2413,12 +2419,6 @@ "group": "WM_DEBUG_RECENTS_ANIMATIONS", "at": "com\/android\/server\/wm\/RecentsAnimationController.java" }, - "90764070": { - "message": "Could not report token removal to the window token client.", - "level": "WARN", - "group": "WM_ERROR", - "at": "com\/android\/server\/wm\/WindowContextListenerController.java" - }, "95216706": { "message": "hideIme target: %s ", "level": "DEBUG", @@ -4333,12 +4333,6 @@ "group": "WM_DEBUG_TASKS", "at": "com\/android\/server\/wm\/RootWindowContainer.java" }, - "1948483534": { - "message": "Could not report config changes to the window token client.", - "level": "WARN", - "group": "WM_ERROR", - "at": "com\/android\/server\/wm\/WindowContextListenerController.java" - }, "1964565370": { "message": "Starting remote animation", "level": "INFO", diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 60854885d5bb..590dbda19c67 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -566,8 +566,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A boolean forceNewConfig; // force re-create with new config next time boolean supportsEnterPipOnTaskSwitch; // This flag is set by the system to indicate that the // activity can enter picture in picture while pausing (only when switching to another task) + // The PiP params used when deferring the entering of picture-in-picture. PictureInPictureParams pictureInPictureArgs = new PictureInPictureParams.Builder().build(); - // The PiP params used when deferring the entering of picture-in-picture. boolean shouldDockBigOverlays; int launchCount; // count of launches since last state long lastLaunchTime; // time of last launch of this activity @@ -1733,6 +1733,16 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mAnimatingActivityRegistry = registry; } + boolean canAutoEnterPip() { + // beforeStopping=false since the actual pip-ing will take place after startPausing() + final boolean activityCanPip = checkEnterPictureInPictureState( + "startActivityUnchecked", false /* beforeStopping */); + + // check if this activity is about to auto-enter pip + return activityCanPip && pictureInPictureArgs != null + && pictureInPictureArgs.isAutoEnterEnabled(); + } + /** * Sets {@link #mLastParentBeforePip} to the current parent Task, it's caller's job to ensure * {@link #getTask()} is set before this is called. diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 42c363085017..52f8997ba76a 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -314,6 +314,8 @@ import java.util.Set; public class ActivityTaskManagerService extends IActivityTaskManager.Stub { private static final String GRAMMATICAL_GENDER_PROPERTY = "persist.sys.grammatical_gender"; private static final String TAG = TAG_WITH_CLASS_NAME ? "ActivityTaskManagerService" : TAG_ATM; + private static final String ENABLE_PIP2_IMPLEMENTATION = + "persist.wm.debug.enable_pip2_implementation"; static final String TAG_ROOT_TASK = TAG + POSTFIX_ROOT_TASK; static final String TAG_SWITCH = TAG + POSTFIX_SWITCH; @@ -3608,6 +3610,28 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } } + /** + * Prepare to enter PiP mode after {@link TransitionController#requestStartTransition}. + * + * @param r activity auto entering pip + * @return true if the activity is about to auto-enter pip or is already in pip mode. + */ + boolean prepareAutoEnterPictureAndPictureMode(ActivityRecord r) { + // If the activity is already in picture in picture mode, then just return early + if (r.inPinnedWindowingMode()) { + return true; + } + + if (r.canAutoEnterPip() && getTransitionController().getCollectingTransition() != null) { + // This will be used later to construct TransitionRequestInfo for Shell to resolve. + // It will also be passed into a direct moveActivityToPinnedRootTask() call via + // startTransition() + getTransitionController().getCollectingTransition().setPipActivity(r); + return true; + } + return false; + } + boolean enterPictureInPictureMode(@NonNull ActivityRecord r, @NonNull PictureInPictureParams params, boolean fromClient) { return enterPictureInPictureMode(r, params, fromClient, false /* isAutoEnter */); @@ -7164,4 +7188,8 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { ActivityTaskManagerService.this.unregisterTaskStackListener(listener); } } + + static boolean isPip2ExperimentEnabled() { + return SystemProperties.getBoolean(ENABLE_PIP2_IMPLEMENTATION, false); + } } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 387a8767ced3..fbbba5a2edec 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -5107,7 +5107,6 @@ class Task extends TaskFragment { void startActivityLocked(ActivityRecord r, @Nullable Task topTask, boolean newTask, boolean isTaskSwitch, ActivityOptions options, @Nullable ActivityRecord sourceRecord) { - final ActivityRecord pipCandidate = findEnterPipOnTaskSwitchCandidate(topTask); Task rTask = r.getTask(); final boolean allowMoveToFront = options == null || !options.getAvoidMoveToFront(); final boolean isOrhasTask = rTask == this || hasChild(rTask); @@ -5171,8 +5170,10 @@ class Task extends TaskFragment { // supporting picture-in-picture while pausing only if the starting activity // would not be considered an overlay on top of the current activity // (eg. not fullscreen, or the assistant) - enableEnterPipOnTaskSwitch(pipCandidate, - null /* toFrontTask */, r, options); + if (!ActivityTaskManagerService.isPip2ExperimentEnabled()) { + final ActivityRecord pipCandidate = findEnterPipOnTaskSwitchCandidate(topTask); + enableEnterPipOnTaskSwitch(pipCandidate, null /* toFrontTask */, r, options); + } } boolean doShow = true; if (newTask) { @@ -5245,7 +5246,7 @@ class Task extends TaskFragment { * enter PiP while it is pausing (if supported). Only one of {@param toFrontTask} or * {@param toFrontActivity} should be set. */ - private static void enableEnterPipOnTaskSwitch(@Nullable ActivityRecord pipCandidate, + static void enableEnterPipOnTaskSwitch(@Nullable ActivityRecord pipCandidate, @Nullable Task toFrontTask, @Nullable ActivityRecord toFrontActivity, @Nullable ActivityOptions opts) { if (pipCandidate == null) { diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 6dc896a92e52..10efb3bee8ef 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -1643,7 +1643,17 @@ class TaskFragment extends WindowContainer { // next activity. final boolean lastResumedCanPip = prev.checkEnterPictureInPictureState( "shouldAutoPipWhilePausing", userLeaving); - if (userLeaving && resumingOccludesParent && lastResumedCanPip + + if (ActivityTaskManagerService.isPip2ExperimentEnabled()) { + // If a new task is being launched, then mark the existing top activity as + // supporting picture-in-picture while pausing only if the starting activity + // would not be considered an overlay on top of the current activity + // (eg. not fullscreen, or the assistant) + Task.enableEnterPipOnTaskSwitch(prev, resuming.getTask(), + resuming, resuming.getOptions()); + } + if (prev.supportsEnterPipOnTaskSwitch && userLeaving + && resumingOccludesParent && lastResumedCanPip && prev.pictureInPictureArgs.isAutoEnterEnabled()) { shouldAutoPip = true; } else if (!lastResumedCanPip) { @@ -1656,7 +1666,12 @@ class TaskFragment extends WindowContainer { } if (prev.attachedToProcess()) { - if (shouldAutoPip) { + if (shouldAutoPip && ActivityTaskManagerService.isPip2ExperimentEnabled()) { + prev.mPauseSchedulePendingForPip = true; + boolean willAutoPip = mAtmService.prepareAutoEnterPictureAndPictureMode(prev); + ProtoLog.d(WM_DEBUG_STATES, "Auto-PIP allowed, requesting PIP mode " + + "via requestStartTransition(): %s, willAutoPip: %b", prev, willAutoPip); + } else if (shouldAutoPip) { prev.mPauseSchedulePendingForPip = true; boolean didAutoPip = mAtmService.enterPictureInPictureMode( prev, prev.pictureInPictureArgs, false /* fromClient */); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 81f91c739bc3..218580b04462 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -174,6 +174,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { private final Token mToken; private IApplicationThread mRemoteAnimApp; + private @Nullable ActivityRecord mPipActivity; + /** Only use for clean-up after binder death! */ private SurfaceControl.Transaction mStartTransaction = null; private SurfaceControl.Transaction mFinishTransaction = null; @@ -508,6 +510,21 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } } + /** + * Set the pip-able activity participating in this transition. + * @param pipActivity activity about to enter pip + */ + void setPipActivity(@Nullable ActivityRecord pipActivity) { + mPipActivity = pipActivity; + } + + /** + * @return pip-able activity participating in this transition. + */ + @Nullable ActivityRecord getPipActivity() { + return mPipActivity; + } + /** * Only set flag to the parent tasks and activity itself. */ diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index e4b9571d0028..b53f1f39b8c2 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -705,13 +705,21 @@ class TransitionController { try { ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Requesting StartTransition: %s", transition); - ActivityManager.RunningTaskInfo info = null; + ActivityManager.RunningTaskInfo startTaskInfo = null; + ActivityManager.RunningTaskInfo pipTaskInfo = null; if (startTask != null) { - info = new ActivityManager.RunningTaskInfo(); - startTask.fillTaskInfo(info); + startTaskInfo = startTask.getTaskInfo(); } - final TransitionRequestInfo request = new TransitionRequestInfo( - transition.mType, info, remoteTransition, displayChange, transition.getFlags()); + + // set the pip task in the request if provided + if (mCollectingTransition.getPipActivity() != null) { + pipTaskInfo = mCollectingTransition.getPipActivity().getTask().getTaskInfo(); + } + + final TransitionRequestInfo request = new TransitionRequestInfo(transition.mType, + startTaskInfo, pipTaskInfo, remoteTransition, displayChange, + transition.getFlags()); + transition.mLogger.mRequestTimeNs = SystemClock.elapsedRealtimeNanos(); transition.mLogger.mRequest = request; mTransitionPlayer.requestStartTransition(transition.getToken(), request); -- cgit v1.2.3-59-g8ed1b From 23b9f3877217cd12107a8247b1aeb6c09db179ac Mon Sep 17 00:00:00 2001 From: Riddle Hsu Date: Wed, 20 Sep 2023 23:13:43 +0800 Subject: Only increase pending relaunch count if schedule is success Otherwise the client won't report finishRelaunching to decrease mPendingRelaunchCount and cause ActivityRecord#isSyncFinished to return false. Also skip pre-loading recents(home) if its process is still cached (e.g. intermediate state when switching user). Otherwise the transaction may be failed by frozen state. Bug: 301034389 Test: atest RecentsAnimationTest#testPreloadRecentsActivity Test: Create multiple users with using different font size, wallpaper, dark theme. Launch several apps on each users. Switch between the users multiple times. There won't be transition timeout when returning from other apps to home. Change-Id: Ia2761e1e9fadf98ab952440ae884c12cc78697c8 --- data/etc/services.core.protolog.json | 6 ------ services/core/java/com/android/server/wm/ActivityRecord.java | 4 ++-- services/core/java/com/android/server/wm/RecentsAnimation.java | 6 ++++++ .../wmtests/src/com/android/server/wm/RecentsAnimationTest.java | 6 ++++-- 4 files changed, 12 insertions(+), 10 deletions(-) (limited to 'data') diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 28a4b49e9d00..e46ba9abc9a6 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -2059,12 +2059,6 @@ "group": "WM_DEBUG_WINDOW_TRANSITIONS_MIN", "at": "com\/android\/server\/wm\/TransitionController.java" }, - "-262984451": { - "message": "Relaunch failed %s", - "level": "INFO", - "group": "WM_DEBUG_STATES", - "at": "com\/android\/server\/wm\/ActivityRecord.java" - }, "-251259736": { "message": "No longer freezing: %s", "level": "VERBOSE", diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 52dafc276711..8ed97f4e826d 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -9888,7 +9888,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A ProtoLog.i(WM_DEBUG_STATES, "Moving to %s Relaunching %s callers=%s" , (andResume ? "RESUMED" : "PAUSED"), this, Debug.getCallers(6)); forceNewConfig = false; - startRelaunching(); final ClientTransactionItem callbackItem = ActivityRelaunchItem.obtain(token, pendingResults, pendingNewIntents, configChangeFlags, new MergedConfiguration(getProcessGlobalConfiguration(), @@ -9905,11 +9904,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A transaction.addCallback(callbackItem); transaction.setLifecycleStateRequest(lifecycleItem); mAtmService.getLifecycleManager().scheduleTransaction(transaction); + startRelaunching(); // Note: don't need to call pauseIfSleepingLocked() here, because the caller will only // request resume if this activity is currently resumed, which implies we aren't // sleeping. } catch (RemoteException e) { - ProtoLog.i(WM_DEBUG_STATES, "Relaunch failed %s", e); + Slog.w(TAG, "Failed to relaunch " + this + ": " + e); } if (andResume) { diff --git a/services/core/java/com/android/server/wm/RecentsAnimation.java b/services/core/java/com/android/server/wm/RecentsAnimation.java index be9058840492..ee05e355e8ef 100644 --- a/services/core/java/com/android/server/wm/RecentsAnimation.java +++ b/services/core/java/com/android/server/wm/RecentsAnimation.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static android.app.ActivityManager.PROCESS_STATE_CACHED_ACTIVITY; import static android.app.ActivityManager.START_TASK_TO_FRONT; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; @@ -117,6 +118,11 @@ class RecentsAnimation implements RecentsAnimationCallbacks, OnRootTaskOrderChan return; } if (targetActivity.attachedToProcess()) { + if (targetActivity.app.getCurrentProcState() >= PROCESS_STATE_CACHED_ACTIVITY) { + Slog.v(TAG, "Skip preload recents for cached proc " + targetActivity.app); + // The process may be frozen that cannot receive binder call. + return; + } // The activity may be relaunched if it cannot handle the current configuration // changes. The activity will be paused state if it is relaunched, otherwise it // keeps the original stopped state. diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java index de3a526573f8..491d5b56c8e2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static android.app.ActivityManager.PROCESS_STATE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; @@ -148,8 +149,9 @@ public class RecentsAnimationTest extends WindowTestsBase { anyInt() /* startFlags */, any() /* profilerInfo */); // Assume its process is alive because the caller should be the recents service. - mSystemServicesTestRule.addProcess(aInfo.packageName, aInfo.processName, 12345 /* pid */, - aInfo.applicationInfo.uid); + final WindowProcessController proc = mSystemServicesTestRule.addProcess(aInfo.packageName, + aInfo.processName, 12345 /* pid */, aInfo.applicationInfo.uid); + proc.setCurrentProcState(PROCESS_STATE_HOME); Intent recentsIntent = new Intent().setComponent(mRecentsComponent); // Null animation indicates to preload. -- cgit v1.2.3-59-g8ed1b From 670fb7f5c0d23cf51ead25538bcb017e03ed73ac Mon Sep 17 00:00:00 2001 From: Chris Göllner Date: Fri, 14 Jul 2023 16:35:06 +0100 Subject: Start logging rotation lock history + include caller information There have been a few reports on foldables where rotation lock suddenly changed, without user interaction. Adding these logs will make it easier to debug the issue. Bug: 289023967 Bug: 289534937 Bug: 279685215 Test: Manually - Change rotation lock and check logs in dumpsys Change-Id: If8de11265355f640a6ec54950bb3250c231b34cf --- core/java/android/app/UiAutomationConnection.java | 11 +++-- core/java/android/view/IWindowManager.aidl | 8 ++-- .../com/android/internal/view/RotationPolicy.java | 20 ++++---- data/etc/services.core.protolog.json | 18 ++++--- .../shared/rotation/RotationButtonController.java | 10 ++-- .../systemui/navigationbar/NavigationBar.java | 3 +- .../systemui/qs/tiles/RotationLockTile.java | 2 +- .../DeviceStateRotationLockSettingController.java | 3 +- .../statusbar/policy/RotationLockController.java | 4 +- .../policy/RotationLockControllerImpl.java | 8 ++-- .../systemui/util/wrapper/RotationPolicyWrapper.kt | 12 ++--- ...viceStateRotationLockSettingControllerTest.java | 14 ++++-- .../utils/leaks/FakeRotationLockController.java | 4 +- .../com/android/server/wm/DisplayRotation.java | 55 ++++++++++++++++++++-- .../wm/DisplayRotationReversionController.java | 3 +- .../android/server/wm/WindowManagerService.java | 22 +++++---- .../server/wm/WindowManagerShellCommand.java | 9 ++-- .../com/android/server/wm/DisplayContentTests.java | 10 ++-- .../android/server/wm/DisplayRotationTests.java | 4 +- .../src/com/android/server/wm/TaskTests.java | 6 +-- .../tests/WindowManagerPermissionTests.java | 4 +- 21 files changed, 154 insertions(+), 76 deletions(-) (limited to 'data') diff --git a/core/java/android/app/UiAutomationConnection.java b/core/java/android/app/UiAutomationConnection.java index 6f4abfdc05c1..6a03c17159d3 100644 --- a/core/java/android/app/UiAutomationConnection.java +++ b/core/java/android/app/UiAutomationConnection.java @@ -207,9 +207,10 @@ public final class UiAutomationConnection extends IUiAutomationConnection.Stub { final long identity = Binder.clearCallingIdentity(); try { if (rotation == UiAutomation.ROTATION_UNFREEZE) { - mWindowManager.thawRotation(); + mWindowManager.thawRotation(/* caller= */ "UiAutomationConnection#setRotation"); } else { - mWindowManager.freezeRotation(rotation); + mWindowManager.freezeRotation(rotation, + /* caller= */ "UiAutomationConnection#setRotation"); } return true; } catch (RemoteException re) { @@ -615,11 +616,13 @@ public final class UiAutomationConnection extends IUiAutomationConnection.Stub { if (mInitialFrozenRotation != INITIAL_FROZEN_ROTATION_UNSPECIFIED) { // Calling out with a lock held is fine since if the system // process is gone the client calling in will be killed. - mWindowManager.freezeRotation(mInitialFrozenRotation); + mWindowManager.freezeRotation(mInitialFrozenRotation, + /* caller= */ "UiAutomationConnection#restoreRotationStateLocked"); } else { // Calling out with a lock held is fine since if the system // process is gone the client calling in will be killed. - mWindowManager.thawRotation(); + mWindowManager.thawRotation( + /* caller= */ "UiAutomationConnection#restoreRotationStateLocked"); } } catch (RemoteException re) { /* ignore */ diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index d3b7a5be47ba..cccac95b9caa 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -316,14 +316,14 @@ interface IWindowManager * android.view.Display#DEFAULT_DISPLAY} and given rotation. */ @UnsupportedAppUsage - void freezeRotation(int rotation); + void freezeRotation(int rotation, String caller); /** * Equivalent to calling {@link #thawDisplayRotation(int)} with {@link * android.view.Display#DEFAULT_DISPLAY}. */ @UnsupportedAppUsage - void thawRotation(); + void thawRotation(String caller); /** * Equivelant to call {@link #isDisplayRotationFrozen(int)} with {@link @@ -341,7 +341,7 @@ interface IWindowManager * {@link android.view.Surface#ROTATION_270} or -1 to freeze it to current rotation. * @hide */ - void freezeDisplayRotation(int displayId, int rotation); + void freezeDisplayRotation(int displayId, int rotation, String caller); /** * Release the orientation lock imposed by freezeRotation() on the display. @@ -349,7 +349,7 @@ interface IWindowManager * @param displayId the ID of display which rotation should be thawed. * @hide */ - void thawDisplayRotation(int displayId); + void thawDisplayRotation(int displayId, String caller); /** * Gets whether the rotation is frozen on the display. diff --git a/core/java/com/android/internal/view/RotationPolicy.java b/core/java/com/android/internal/view/RotationPolicy.java index 058c6ec4d13c..6e45796df053 100644 --- a/core/java/com/android/internal/view/RotationPolicy.java +++ b/core/java/com/android/internal/view/RotationPolicy.java @@ -105,23 +105,23 @@ public final class RotationPolicy { /** * Enables or disables rotation lock from the system UI toggle. */ - public static void setRotationLock(Context context, final boolean enabled) { + public static void setRotationLock(Context context, final boolean enabled, String caller) { final int rotation = areAllRotationsAllowed(context) || useCurrentRotationOnRotationLockChange(context) ? CURRENT_ROTATION : NATURAL_ROTATION; - setRotationLockAtAngle(context, enabled, rotation); + setRotationLockAtAngle(context, enabled, rotation, caller); } /** * Enables or disables rotation lock at a specific rotation from system UI. */ public static void setRotationLockAtAngle(Context context, final boolean enabled, - final int rotation) { + final int rotation, String caller) { Settings.System.putIntForUser(context.getContentResolver(), Settings.System.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY, 0, UserHandle.USER_CURRENT); - setRotationLock(enabled, rotation); + setRotationLock(enabled, rotation, caller); } /** @@ -129,12 +129,13 @@ public final class RotationPolicy { * * If rotation is locked for accessibility, the system UI toggle is hidden to avoid confusion. */ - public static void setRotationLockForAccessibility(Context context, final boolean enabled) { + public static void setRotationLockForAccessibility(Context context, final boolean enabled, + String caller) { Settings.System.putIntForUser(context.getContentResolver(), Settings.System.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY, enabled ? 1 : 0, UserHandle.USER_CURRENT); - setRotationLock(enabled, NATURAL_ROTATION); + setRotationLock(enabled, NATURAL_ROTATION, caller); } private static boolean areAllRotationsAllowed(Context context) { @@ -146,16 +147,17 @@ public final class RotationPolicy { R.bool.config_useCurrentRotationOnRotationLockChange); } - private static void setRotationLock(final boolean enabled, final int rotation) { + private static void setRotationLock(final boolean enabled, final int rotation, + final String caller) { AsyncTask.execute(new Runnable() { @Override public void run() { try { IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); if (enabled) { - wm.freezeRotation(rotation); + wm.freezeRotation(rotation, caller); } else { - wm.thawRotation(); + wm.thawRotation(caller); } } catch (RemoteException exc) { Log.w(TAG, "Unable to save auto-rotate setting"); diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 28a4b49e9d00..417d374150ca 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -1123,12 +1123,6 @@ "group": "WM_SHOW_TRANSACTIONS", "at": "com\/android\/server\/wm\/WindowSurfaceController.java" }, - "-1076978367": { - "message": "thawRotation: mRotation=%d", - "level": "VERBOSE", - "group": "WM_DEBUG_ORIENTATION", - "at": "com\/android\/server\/wm\/WindowManagerService.java" - }, "-1075136930": { "message": "startLockTaskMode: Can't lock due to auth", "level": "WARN", @@ -1231,6 +1225,12 @@ "group": "WM_DEBUG_STARTING_WINDOW", "at": "com\/android\/server\/wm\/WindowState.java" }, + "-962760979": { + "message": "thawRotation: mRotation=%d, caller=%s", + "level": "VERBOSE", + "group": "WM_DEBUG_ORIENTATION", + "at": "com\/android\/server\/wm\/WindowManagerService.java" + }, "-961053385": { "message": "attachWindowContextToDisplayArea: calling from non-existing process pid=%d uid=%d", "level": "WARN", @@ -2785,6 +2785,12 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "364992694": { + "message": "freezeDisplayRotation: current rotation=%d, new rotation=%d, caller=%s", + "level": "VERBOSE", + "group": "WM_DEBUG_ORIENTATION", + "at": "com\/android\/server\/wm\/WindowManagerService.java" + }, "371173718": { "message": "finishSync cancel=%b for %s", "level": "VERBOSE", diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java index 905923039f8b..c0749885846f 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java @@ -282,9 +282,9 @@ public class RotationButtonController { TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener); } - public void setRotationLockedAtAngle(int rotationSuggestion) { + public void setRotationLockedAtAngle(int rotationSuggestion, String caller) { RotationPolicy.setRotationLockAtAngle(mContext, /* enabled= */ isRotationLocked(), - /* rotation= */ rotationSuggestion); + /* rotation= */ rotationSuggestion, caller); } public boolean isRotationLocked() { @@ -468,7 +468,8 @@ public class RotationButtonController { if (rotationLocked || mRotationButton.isVisible()) { // Do not allow a change in rotation to set user rotation when docked. if (shouldOverrideUserLockPrefs(rotation) && rotationLocked && !mDocked) { - setRotationLockedAtAngle(rotation); + setRotationLockedAtAngle(rotation, /* caller= */ + "RotationButtonController#onRotationWatcherChanged"); } setRotateSuggestionButtonState(false /* visible */, true /* forced */); } @@ -572,7 +573,8 @@ public class RotationButtonController { private void onRotateSuggestionClick(View v) { mUiEventLogger.log(RotationButtonEvent.ROTATION_SUGGESTION_ACCEPTED); incrementNumAcceptedRotationSuggestionsIfNeeded(); - setRotationLockedAtAngle(mLastRotationSuggestion); + setRotationLockedAtAngle(mLastRotationSuggestion, + /* caller= */ "RotationButtonController#onRotateSuggestionClick"); Log.i(TAG, "onRotateSuggestionClick() mLastRotationSuggestion=" + mLastRotationSuggestion); v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index 62b22c50c1dc..f95200b0568d 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -795,7 +795,8 @@ public class NavigationBar extends ViewController implements // Reset user rotation pref to match that of the WindowManager if starting in locked // mode. This will automatically happen when switching from auto-rotate to locked mode. if (display != null && rotationButtonController.isRotationLocked()) { - rotationButtonController.setRotationLockedAtAngle(display.getRotation()); + rotationButtonController.setRotationLockedAtAngle( + display.getRotation(), /* caller= */ "NavigationBar#onViewAttached"); } } else { mDisabledFlags2 |= StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS; diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java index 2d9f7dd038bc..a2fa165f4bc5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java @@ -133,7 +133,7 @@ public class RotationLockTile extends QSTileImpl implements @Override protected void handleClick(@Nullable View view) { final boolean newState = !mState.value; - mController.setRotationLocked(!newState); + mController.setRotationLocked(!newState, /* caller= */ "RotationLockTile#handleClick"); refreshState(newState); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java index 01fabcc8bc1e..3008c866d207 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java @@ -159,7 +159,8 @@ public final class DeviceStateRotationLockSettingController // Update the rotation policy, if needed, for this new device state if (shouldBeLocked != isLocked) { - mRotationPolicyWrapper.setRotationLock(shouldBeLocked); + mRotationPolicyWrapper.setRotationLock(shouldBeLocked, + /* caller= */"DeviceStateRotationLockSettingController#readPersistedSetting"); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java index 1158324567ff..607f1e562468 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java @@ -24,8 +24,8 @@ public interface RotationLockController extends Listenable, boolean isRotationLockAffordanceVisible(); boolean isRotationLocked(); boolean isCameraRotationEnabled(); - void setRotationLocked(boolean locked); - void setRotationLockedAtAngle(boolean locked, int rotation); + void setRotationLocked(boolean locked, String caller); + void setRotationLockedAtAngle(boolean locked, int rotation, String caller); public interface RotationLockControllerCallback { void onRotationLockStateChanged(boolean rotationLocked, boolean affordanceVisible); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java index 1eeb0ac8b3bb..797aa1f3a3dd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java @@ -93,12 +93,12 @@ public final class RotationLockControllerImpl implements RotationLockController return mRotationPolicy.isCameraRotationEnabled(); } - public void setRotationLocked(boolean locked) { - mRotationPolicy.setRotationLock(locked); + public void setRotationLocked(boolean locked, String caller) { + mRotationPolicy.setRotationLock(locked, caller); } - public void setRotationLockedAtAngle(boolean locked, int rotation) { - mRotationPolicy.setRotationLockAtAngle(locked, rotation); + public void setRotationLockedAtAngle(boolean locked, int rotation, String caller) { + mRotationPolicy.setRotationLockAtAngle(locked, rotation, caller); } public boolean isRotationLockAffordanceVisible() { diff --git a/packages/SystemUI/src/com/android/systemui/util/wrapper/RotationPolicyWrapper.kt b/packages/SystemUI/src/com/android/systemui/util/wrapper/RotationPolicyWrapper.kt index d8de07d185c6..374ebe0f28fb 100644 --- a/packages/SystemUI/src/com/android/systemui/util/wrapper/RotationPolicyWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/util/wrapper/RotationPolicyWrapper.kt @@ -28,8 +28,8 @@ import javax.inject.Inject * Testable wrapper interface around RotationPolicy {link com.android.internal.view.RotationPolicy} */ interface RotationPolicyWrapper { - fun setRotationLock(enabled: Boolean) - fun setRotationLockAtAngle(enabled: Boolean, rotation: Int) + fun setRotationLock(enabled: Boolean, caller: String) + fun setRotationLockAtAngle(enabled: Boolean, rotation: Int, caller: String) fun getRotationLockOrientation(): Int fun isRotationLockToggleVisible(): Boolean fun isRotationLocked(): Boolean @@ -44,14 +44,14 @@ class RotationPolicyWrapperImpl @Inject constructor( ) : RotationPolicyWrapper { - override fun setRotationLock(enabled: Boolean) { + override fun setRotationLock(enabled: Boolean, caller: String) { traceSection("RotationPolicyWrapperImpl#setRotationLock") { - RotationPolicy.setRotationLock(context, enabled) + RotationPolicy.setRotationLock(context, enabled, caller) } } - override fun setRotationLockAtAngle(enabled: Boolean, rotation: Int) { - RotationPolicy.setRotationLockAtAngle(context, enabled, rotation) + override fun setRotationLockAtAngle(enabled: Boolean, rotation: Int, caller: String) { + RotationPolicy.setRotationLockAtAngle(context, enabled, rotation, caller) } override fun getRotationLockOrientation(): Int = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java index c8f28bc17926..4ccbd1b739f3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java @@ -65,7 +65,7 @@ public class DeviceStateRotationLockSettingControllerTest extends SysuiTestCase private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); private final FakeExecutor mFakeExecutor = new FakeExecutor(mFakeSystemClock); - private final RotationPolicyWrapper mFakeRotationPolicy = new FakeRotationPolicy(); + private final FakeRotationPolicy mFakeRotationPolicy = new FakeRotationPolicy(); private DeviceStateRotationLockSettingController mDeviceStateRotationLockSettingController; private DeviceStateManager.DeviceStateCallback mDeviceStateCallback; private DeviceStateRotationLockSettingsManager mSettingsManager; @@ -324,13 +324,21 @@ public class DeviceStateRotationLockSettingControllerTest extends SysuiTestCase private boolean mRotationLock; - @Override public void setRotationLock(boolean enabled) { - mRotationLock = enabled; + setRotationLock(enabled, /* caller= */ "FakeRotationPolicy"); } @Override + public void setRotationLock(boolean enabled, String caller) { + mRotationLock = enabled; + } + public void setRotationLockAtAngle(boolean enabled, int rotation) { + setRotationLockAtAngle(enabled, rotation, /* caller= */ "FakeRotationPolicy"); + } + + @Override + public void setRotationLockAtAngle(boolean enabled, int rotation, String caller) { mRotationLock = enabled; } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeRotationLockController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeRotationLockController.java index 4f9cb35db1a3..be57658a4266 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeRotationLockController.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeRotationLockController.java @@ -46,7 +46,7 @@ public class FakeRotationLockController extends BaseLeakChecker Date: Tue, 19 Sep 2023 16:08:19 +0000 Subject: Remove record_task_content feature flag check in ContentRecorder Feature flag check is no longer needed and leads to scenarios where partial screen sharing fails despite the device being capable of it, so its better to remove this extra point of failure. Bug: 301273469 Test: manually built & smoke test Test: atest WmTests:ContentRecorderTests Change-Id: Iad8926c377a2cae7c1b08f874926d09cc46274a4 --- data/etc/services.core.protolog.json | 6 --- .../com/android/server/wm/ContentRecorder.java | 14 ------- .../android/server/wm/ContentRecorderTests.java | 47 ---------------------- 3 files changed, 67 deletions(-) (limited to 'data') diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index e46ba9abc9a6..ad0ead78f492 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -3973,12 +3973,6 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, - "1563836923": { - "message": "Content Recording: Unable to record task since feature is disabled %d", - "level": "VERBOSE", - "group": "WM_DEBUG_CONTENT_RECORDING", - "at": "com\/android\/server\/wm\/ContentRecorder.java" - }, "1577579529": { "message": "win=%s destroySurfaces: appStopped=%b win.mWindowRemovalAllowed=%b win.mRemoveOnExit=%b", "level": "ERROR", diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java index b499dad53326..06448d0c1e84 100644 --- a/services/core/java/com/android/server/wm/ContentRecorder.java +++ b/services/core/java/com/android/server/wm/ContentRecorder.java @@ -33,7 +33,6 @@ import android.media.projection.IMediaProjectionManager; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; -import android.provider.DeviceConfig; import android.view.ContentRecordingSession; import android.view.ContentRecordingSession.RecordContent; import android.view.Display; @@ -47,11 +46,6 @@ import com.android.internal.protolog.common.ProtoLog; */ final class ContentRecorder implements WindowContainerListener { - /** - * The key for accessing the device config that controls if task recording is supported. - */ - @VisibleForTesting static final String KEY_RECORD_TASK_FEATURE = "record_task_content"; - /** * The display content this class is handling recording for. */ @@ -411,14 +405,6 @@ final class ContentRecorder implements WindowContainerListener { // TODO(206461622) Migrate to using the RootDisplayArea return dc; case RECORD_CONTENT_TASK: - if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER, - KEY_RECORD_TASK_FEATURE, false)) { - handleStartRecordingFailed(); - ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, - "Content Recording: Unable to record task since feature is disabled %d", - mDisplayContent.getDisplayId()); - return null; - } // Given the WindowToken of the region to record, retrieve the associated // SurfaceControl. if (tokenToRecord == null) { diff --git a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java index 3d3531e92abe..d2eb1cc0222b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java @@ -31,7 +31,6 @@ 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.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; -import static com.android.server.wm.ContentRecorder.KEY_RECORD_TASK_FEATURE; import static com.google.common.truth.Truth.assertThat; @@ -51,25 +50,21 @@ import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; import android.platform.test.annotations.Presubmit; -import android.provider.DeviceConfig; import android.view.ContentRecordingSession; import android.view.DisplayInfo; import android.view.Gravity; import android.view.SurfaceControl; -import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; import com.android.server.wm.ContentRecorder.MediaProjectionManagerWrapper; -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 java.util.concurrent.CountDownLatch; /** * Tests for the {@link ContentRecorder} class. @@ -93,9 +88,6 @@ public class ContentRecorderTests extends WindowTestsBase { private ContentRecorder mContentRecorder; @Mock private MediaProjectionManagerWrapper mMediaProjectionManagerWrapper; private SurfaceControl mRecordedSurface; - // Handle feature flag. - private ConfigListener mConfigListener; - private CountDownLatch mLatch; @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -133,23 +125,11 @@ public class ContentRecorderTests extends WindowTestsBase { mWaitingDisplaySession.setVirtualDisplayId(displayId); mWaitingDisplaySession.setWaitingForConsent(true); - mConfigListener = new ConfigListener(); - DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_WINDOW_MANAGER, - mContext.getMainExecutor(), mConfigListener); - mLatch = new CountDownLatch(1); - DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_RECORD_TASK_FEATURE, - "true", true); - // Skip unnecessary operations of relayout. spyOn(mWm.mWindowPlacerLocked); doNothing().when(mWm.mWindowPlacerLocked).performSurfacePlacement(anyBoolean()); } - @After - public void teardown() { - DeviceConfig.removeOnPropertiesChangedListener(mConfigListener); - } - @Test public void testIsCurrentlyRecording() { assertThat(mContentRecorder.isCurrentlyRecording()).isFalse(); @@ -183,24 +163,6 @@ public class ContentRecorderTests extends WindowTestsBase { assertThat(mContentRecorder.isCurrentlyRecording()).isFalse(); } - @Test - public void testUpdateRecording_task_featureDisabled() { - mLatch = new CountDownLatch(1); - DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_RECORD_TASK_FEATURE, - "false", false); - mContentRecorder.setContentRecordingSession(mTaskSession); - mContentRecorder.updateRecording(); - assertThat(mContentRecorder.isCurrentlyRecording()).isFalse(); - } - - @Test - public void testUpdateRecording_task_featureEnabled() { - // Feature already enabled; don't need to again. - mContentRecorder.setContentRecordingSession(mTaskSession); - mContentRecorder.updateRecording(); - assertThat(mContentRecorder.isCurrentlyRecording()).isTrue(); - } - @Test public void testUpdateRecording_task_nullToken() { ContentRecordingSession session = mTaskSession; @@ -703,13 +665,4 @@ public class ContentRecorderTests extends WindowTestsBase { anyInt()); return mirroredSurface; } - - private class ConfigListener implements DeviceConfig.OnPropertiesChangedListener { - @Override - public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) { - if (mLatch != null && properties.getKeyset().contains(KEY_RECORD_TASK_FEATURE)) { - mLatch.countDown(); - } - } - } } -- cgit v1.2.3-59-g8ed1b From 45f626a154f66f4cceecaf6e32392741a00529a3 Mon Sep 17 00:00:00 2001 From: Arpit Singh Date: Tue, 26 Sep 2023 09:59:23 +0000 Subject: Mark stylus buttons mapppings as fallback We don't have a way to determine if devices can actually report HID usage keys. Marking them as fallback only. Bug: 297094448 Test: atest inputflinger_tests Change-Id: I45710f9e6237c86613717fab779dbd7cc5c66c86 --- data/keyboards/Generic.kl | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) (limited to 'data') diff --git a/data/keyboards/Generic.kl b/data/keyboards/Generic.kl index 31092536bac5..51b720ddb758 100644 --- a/data/keyboards/Generic.kl +++ b/data/keyboards/Generic.kl @@ -430,19 +430,17 @@ key 658 MACRO_3 key 659 MACRO_4 # Keys defined by HID usages -key usage 0x0c0067 WINDOW -key usage 0x0c006F BRIGHTNESS_UP -key usage 0x0c0070 BRIGHTNESS_DOWN -key usage 0x0c0079 KEYBOARD_BACKLIGHT_UP -key usage 0x0c007A KEYBOARD_BACKLIGHT_DOWN -key usage 0x0c007C KEYBOARD_BACKLIGHT_TOGGLE -key usage 0x0c0173 MEDIA_AUDIO_TRACK -key usage 0x0c019C PROFILE_SWITCH -key usage 0x0c01A2 ALL_APPS -# TODO(b/297094448): Add stylus button mappings as a fallback when we have a way to determine -# if a device can actually report it. -# key usage 0x0d0044 STYLUS_BUTTON_PRIMARY -# key usage 0x0d005a STYLUS_BUTTON_SECONDARY +key usage 0x0c0067 WINDOW FALLBACK_USAGE_MAPPING +key usage 0x0c006F BRIGHTNESS_UP FALLBACK_USAGE_MAPPING +key usage 0x0c0070 BRIGHTNESS_DOWN FALLBACK_USAGE_MAPPING +key usage 0x0c0079 KEYBOARD_BACKLIGHT_UP FALLBACK_USAGE_MAPPING +key usage 0x0c007A KEYBOARD_BACKLIGHT_DOWN FALLBACK_USAGE_MAPPING +key usage 0x0c007C KEYBOARD_BACKLIGHT_TOGGLE FALLBACK_USAGE_MAPPING +key usage 0x0c0173 MEDIA_AUDIO_TRACK FALLBACK_USAGE_MAPPING +key usage 0x0c019C PROFILE_SWITCH FALLBACK_USAGE_MAPPING +key usage 0x0c01A2 ALL_APPS FALLBACK_USAGE_MAPPING +key usage 0x0d0044 STYLUS_BUTTON_PRIMARY FALLBACK_USAGE_MAPPING +key usage 0x0d005a STYLUS_BUTTON_SECONDARY FALLBACK_USAGE_MAPPING # Joystick and game controller axes. # Axes that are not mapped will be assigned generic axis numbers by the input subsystem. -- cgit v1.2.3-59-g8ed1b From a8f6d75cf2e5cdacb9fdf86cd9cf5b37e25de448 Mon Sep 17 00:00:00 2001 From: Victor Hsieh Date: Fri, 22 Sep 2023 11:36:34 -0700 Subject: Ensure signature for allowlisted system app update on boot With v4 signature and fs-verity, verifying an APK integrity is O(1) time. This allows us to enforce signature and detect persistent attack (via tampering with an APK) across a reboot for updated system apps. For the first step, we don't implement a policy (e.g. all priv apps) and only protect the packages in an allowlist specified by a resource property. This is due to the ecosystem complexity, where some OEM may preload app from another developer, since developer's v4 signature may need extra plumbing depending on how they are installed. Some implementation details: After a system package is updated, during the boot we still want to retrieve the SigningDetails from the APK in the dm-verity protected partition. This is to harden and protect the allowlisted apps from tampered record in an attacking scenario. The SigningDetails of the disabled pacakge is then used during the reconcile phase, to ensure the updated package has consistent signature with the original version. Originally, canSkipForcedPackageVerification checks splits explicitly. This is not necessary because ParsingPackageUtils.getSigningDetails can only succeed (e.g. during collectCertificatesLI, when forced) if the splits are consistent with the base. Delete some dead code, e.g. in the skipVerify condition. Bug: 277344944 Test: 1. locally add com.android.egg to the allowlist 2. build EasterEgg with v4 signature; and EasterEgg2 with a different signing key 3. adb install-multiple --no-incr EasterEgg.apk EasterEgg.apk.idsig 4. with root, replace base.apk and base.apk.idsig with EasterEgg2, chown and enable fsverity 5. adb shell stop/start 6. verify from logcat that the APK is recovered by expected check * With some code change to force condition. Change-Id: I0b62b73208c7d4e6b8613f1ae3aa726de8d8fa65 --- data/etc/Android.bp | 6 +++ .../etc/preinstalled-packages-strict-signature.xml | 27 +++++++++++ .../core/java/com/android/server/SystemConfig.java | 19 ++++++++ .../android/server/pm/InstallPackageHelper.java | 55 ++++++++++++---------- .../java/com/android/server/pm/InstallRequest.java | 4 +- .../server/pm/PackageManagerServiceUtils.java | 31 ++++++------ .../android/server/pm/SharedLibrariesImplTest.kt | 6 +-- 7 files changed, 102 insertions(+), 46 deletions(-) create mode 100644 data/etc/preinstalled-packages-strict-signature.xml (limited to 'data') diff --git a/data/etc/Android.bp b/data/etc/Android.bp index 6a1f3f959185..ade20d282acc 100644 --- a/data/etc/Android.bp +++ b/data/etc/Android.bp @@ -60,6 +60,12 @@ prebuilt_etc { src: "preinstalled-packages-asl-files.xml", } +prebuilt_etc { + name: "preinstalled-packages-strict-signature.xml", + sub_dir: "sysconfig", + src: "preinstalled-packages-strict-signature.xml", +} + // Privapp permission whitelist files prebuilt_etc { diff --git a/data/etc/preinstalled-packages-strict-signature.xml b/data/etc/preinstalled-packages-strict-signature.xml new file mode 100644 index 000000000000..3cbfa8c4dbe9 --- /dev/null +++ b/data/etc/preinstalled-packages-strict-signature.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java index caf16849de28..40b29d7b09d5 100644 --- a/services/core/java/com/android/server/SystemConfig.java +++ b/services/core/java/com/android/server/SystemConfig.java @@ -340,6 +340,10 @@ public class SystemConfig { // A map of preloaded package names and the path to its app metadata file path. private final ArrayMap mAppMetadataFilePaths = new ArrayMap<>(); + // A set of pre-installed package names that requires strict signature verification once + // updated to avoid cached/potentially tampered results. + private final Set mPreinstallPackagesWithStrictSignatureCheck = new ArraySet<>(); + /** * Map of system pre-defined, uniquely named actors; keys are namespace, * value maps actor name to package name. @@ -542,6 +546,10 @@ public class SystemConfig { return mAppMetadataFilePaths; } + public Set getPreinstallPackagesWithStrictSignatureCheck() { + return mPreinstallPackagesWithStrictSignatureCheck; + } + /** * Only use for testing. Do NOT use in production code. * @param readPermissions false to create an empty SystemConfig; true to read the permissions. @@ -1485,6 +1493,17 @@ public class SystemConfig { mAppMetadataFilePaths.put(packageName, path); } } break; + case "require-strict-signature": { + if (android.security.Flags.extendVbChainToUpdatedApk()) { + String packageName = parser.getAttributeValue(null, "package"); + if (TextUtils.isEmpty(packageName)) { + Slog.w(TAG, "<" + name + "> without valid package in " + permFile + + " at " + parser.getPositionDescription()); + } else { + mPreinstallPackagesWithStrictSignatureCheck.add(packageName); + } + } + } break; default: { Slog.w(TAG, "Tag " + name + " is unknown in " + permFile + " at " + parser.getPositionDescription()); diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 8f71a9be4fe5..c6388e7bede7 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -3728,7 +3728,7 @@ final class InstallPackageHelper { final ScanResult scanResult = scanResultPair.first; boolean shouldHideSystemApp = scanResultPair.second; final InstallRequest installRequest = new InstallRequest( - parsedPackage, parseFlags, scanFlags, user, scanResult); + parsedPackage, parseFlags, scanFlags, user, scanResult, disabledPkgSetting); String existingApexModuleName = null; synchronized (mPm.mLock) { @@ -3962,6 +3962,7 @@ final class InstallPackageHelper { final String disabledPkgName = pkgAlreadyExists ? pkgSetting.getPackageName() : parsedPackage.getPackageName(); final boolean isSystemPkgUpdated; + final PackageSetting disabledPkgSetting; final boolean isUpgrade; synchronized (mPm.mLock) { isUpgrade = mPm.isDeviceUpgrading(); @@ -3975,8 +3976,7 @@ final class InstallPackageHelper { + "and install it as non-updated system app."); mPm.mSettings.removeDisabledSystemPackageLPw(disabledPkgName); } - final PackageSetting disabledPkgSetting = - mPm.mSettings.getDisabledSystemPkgLPr(disabledPkgName); + disabledPkgSetting = mPm.mSettings.getDisabledSystemPkgLPr(disabledPkgName); isSystemPkgUpdated = disabledPkgSetting != null; if (DEBUG_INSTALL && isSystemPkgUpdated) { @@ -4048,6 +4048,23 @@ final class InstallPackageHelper { // equal to the version on the /data partition. Throw an exception and use // the application already installed on the /data partition. if (scanSystemPartition && isSystemPkgUpdated && !isSystemPkgBetter) { + // For some updated system packages, during addForInit we want to ensure the + // PackageSetting has the correct SigningDetails compares to the original version on + // the system partition. For the check to happen later during the /data scan, update + // the disabled package setting per the original APK on a system partition so that it + // can be trusted during reconcile. + if (needSignatureMatchToSystem(parsedPackage.getPackageName())) { + final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); + final ParseResult result = + ParsingPackageUtils.getSigningDetails(input, parsedPackage, + false /*skipVerify*/); + if (result.isError()) { + throw new PrepareFailure("Failed collect during scanSystemPackageLI", + result.getException()); + } + disabledPkgSetting.setSigningDetails(result.getResult()); + } + // In the case of a skipped package, commitReconciledScanResultLocked is not called to // add the object to the "live" data structures, so this is the final mutation step // for the package. Which means it needs to be finalized here to cache derived fields. @@ -4065,19 +4082,16 @@ final class InstallPackageHelper { // Verify certificates against what was last scanned. Force re-collecting certificate in two // special cases: // 1) when scanning system, force re-collect only if system is upgrading. - // 2) when scanning /data, force re-collect only if the app is privileged (updated from - // preinstall, or treated as privileged, e.g. due to shared user ID). + // 2) when scanning /data, force re-collect only if the package name is allowlisted. final boolean forceCollect = scanSystemPartition ? isUpgrade - : PackageManagerServiceUtils.isApkVerificationForced(pkgSetting); + : pkgAlreadyExists && needSignatureMatchToSystem(pkgSetting.getPackageName()); if (DEBUG_VERIFY && forceCollect) { Slog.d(TAG, "Force collect certificate of " + parsedPackage.getPackageName()); } - // Full APK verification can be skipped during certificate collection, only if the file is - // in verified partition, or can be verified on access (when apk verity is enabled). In both - // cases, only data in Signing Block is verified instead of the whole file. - final boolean skipVerify = scanSystemPartition - || (forceCollect && canSkipForcedPackageVerification(parsedPackage)); + // APK verification can be skipped during certificate collection, only if the file is in a + // verified partition. + final boolean skipVerify = scanSystemPartition; ScanPackageUtils.collectCertificatesLI(pkgSetting, parsedPackage, mPm.getSettingsVersionForPackage(parsedPackage), forceCollect, skipVerify, mPm.isPreNMR1Upgrade()); @@ -4196,22 +4210,15 @@ final class InstallPackageHelper { } /** - * Returns if forced apk verification can be skipped for the whole package, including splits. + * Returns whether the package needs a signature verification against the pre-installed version + * at boot. */ - private boolean canSkipForcedPackageVerification(AndroidPackage pkg) { - if (!VerityUtils.hasFsverity(pkg.getBaseApkPath())) { + private boolean needSignatureMatchToSystem(String packageName) { + if (!android.security.Flags.extendVbChainToUpdatedApk()) { return false; } - // TODO: Allow base and splits to be verified individually. - String[] splitCodePaths = pkg.getSplitCodePaths(); - if (!ArrayUtils.isEmpty(splitCodePaths)) { - for (int i = 0; i < splitCodePaths.length; i++) { - if (!VerityUtils.hasFsverity(splitCodePaths[i])) { - return false; - } - } - } - return true; + return mPm.mInjector.getSystemConfig().getPreinstallPackagesWithStrictSignatureCheck() + .contains(packageName); } /** diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java index a4ee3c8287c5..1c7024b7d239 100644 --- a/services/core/java/com/android/server/pm/InstallRequest.java +++ b/services/core/java/com/android/server/pm/InstallRequest.java @@ -58,7 +58,6 @@ import com.android.server.pm.pkg.parsing.ParsingPackageUtils; import java.io.File; import java.util.ArrayList; -import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; @@ -190,7 +189,7 @@ final class InstallRequest { // addForInit InstallRequest(ParsedPackage parsedPackage, int parseFlags, int scanFlags, - @Nullable UserHandle user, ScanResult scanResult) { + @Nullable UserHandle user, ScanResult scanResult, PackageSetting disabledPs) { if (user != null) { mUserId = user.getIdentifier(); } else { @@ -206,6 +205,7 @@ final class InstallRequest { mPackageMetrics = null; // No logging from this code path mSessionId = -1; mRequireUserAction = USER_ACTION_UNSPECIFIED; + mDisabledPs = disabledPs; } @Nullable diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java index 8e91f42d5a2a..bcb7bdebf9bf 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java @@ -521,11 +521,8 @@ public class PackageManagerServiceUtils { } /** - * Make sure the updated priv app is signed with the same key as the original APK file on the - * /system partition. - * - *

The rationale is that {@code disabledPkg} is a PackageSetting backed by xml files in /data - * and is not tamperproof. + * Verifies the updated system app has a signature that is consistent with the pre-installed + * version or the signing lineage. */ private static boolean matchSignatureInSystem(@NonNull String packageName, @NonNull SigningDetails signingDetails, PackageSetting disabledPkgSetting) { @@ -559,17 +556,12 @@ public class PackageManagerServiceUtils { == FSVERITY_ENABLED; } - /** Returns true to force apk verification if the package is considered privileged. */ - static boolean isApkVerificationForced(@Nullable PackageSetting ps) { - // TODO(b/154310064): re-enable. - return false; - } - /** * Verifies that signatures match. * @returns {@code true} if the compat signatures were matched; otherwise, {@code false}. * @throws PackageManagerException if the signatures did not match. */ + @SuppressWarnings("ReferenceEquality") public static boolean verifySignatures(PackageSetting pkgSetting, @Nullable SharedUserSetting sharedUserSetting, PackageSetting disabledPkgSetting, SigningDetails parsedSignatures, @@ -578,13 +570,23 @@ public class PackageManagerServiceUtils { final String packageName = pkgSetting.getPackageName(); boolean compatMatch = false; if (pkgSetting.getSigningDetails().getSignatures() != null) { - // Already existing package. Make sure signatures match + // For an already existing package, make sure the parsed signatures from the package + // match the one in PackageSetting. boolean match = parsedSignatures.checkCapability( pkgSetting.getSigningDetails(), SigningDetails.CertCapabilities.INSTALLED_DATA) || pkgSetting.getSigningDetails().checkCapability( parsedSignatures, SigningDetails.CertCapabilities.ROLLBACK); + // Also make sure the parsed signatures are consistent with the disabled package + // setting, if any. The additional UNKNOWN check is because disabled package settings + // may not have SigningDetails currently, and we don't want to cause an uninstall. + if (android.security.Flags.extendVbChainToUpdatedApk() + && match && disabledPkgSetting != null + && disabledPkgSetting.getSigningDetails() != SigningDetails.UNKNOWN) { + match = matchSignatureInSystem(packageName, parsedSignatures, disabledPkgSetting); + } + if (!match && compareCompat) { match = matchSignaturesCompat(packageName, pkgSetting.getSignatures(), parsedSignatures); @@ -603,11 +605,6 @@ public class PackageManagerServiceUtils { SigningDetails.CertCapabilities.ROLLBACK); } - if (!match && isApkVerificationForced(disabledPkgSetting)) { - match = matchSignatureInSystem(packageName, pkgSetting.getSigningDetails(), - disabledPkgSetting); - } - if (!match && isRollback) { // Since a rollback can only be initiated for an APK previously installed on the // device allow rolling back to a previous signing key even if the rollback diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt index 6c392751abe6..b8f726b393cc 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt +++ b/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt @@ -224,7 +224,7 @@ class SharedLibrariesImplTest { val scanRequest = ScanRequest(parsedPackage, null, null, null, null, null, null, null, 0, 0, false, null, null) val scanResult = ScanResult(scanRequest, null, null, false, 0, null, null, null) - var installRequest = InstallRequest(parsedPackage, 0, 0, UserHandle(0), scanResult) + var installRequest = InstallRequest(parsedPackage, 0, 0, UserHandle(0), scanResult, null) val latestInfoSetting = mSharedLibrariesImpl.getStaticSharedLibLatestVersionSetting(installRequest)!! @@ -309,7 +309,7 @@ class SharedLibrariesImplTest { val testInfo = libOfStatic(TEST_LIB_PACKAGE_NAME, TEST_LIB_NAME, 1L) val scanResult = ScanResult(mock(), null, null, false, 0, null, testInfo, null) - var installRequest = InstallRequest(mock(), 0, 0, UserHandle(0), scanResult) + var installRequest = InstallRequest(mock(), 0, 0, UserHandle(0), scanResult, null) val allowedInfos = mSharedLibrariesImpl.getAllowedSharedLibInfos(installRequest) @@ -332,7 +332,7 @@ class SharedLibrariesImplTest { null, null, null, 0, 0, false, null, null) val scanResult = ScanResult(scanRequest, packageSetting, null, false, 0, null, null, listOf(testInfo)) - var installRequest = InstallRequest(parsedPackage, 0, 0, UserHandle(0), scanResult) + var installRequest = InstallRequest(parsedPackage, 0, 0, UserHandle(0), scanResult, null) val allowedInfos = mSharedLibrariesImpl.getAllowedSharedLibInfos(installRequest) -- cgit v1.2.3-59-g8ed1b From a8992021221d9625584e993e1078248a6ef73568 Mon Sep 17 00:00:00 2001 From: Philip Junker Date: Thu, 28 Sep 2023 16:17:47 +0200 Subject: Remove MACRO_1 mapping from default reference remote kl file. Bug: 290068850 Test: manual Change-Id: I394f38d6960deae36237b3a7122fd343dc7a1934 --- data/keyboards/Vendor_0957_Product_0001.kl | 1 - 1 file changed, 1 deletion(-) (limited to 'data') diff --git a/data/keyboards/Vendor_0957_Product_0001.kl b/data/keyboards/Vendor_0957_Product_0001.kl index 354f10a9432a..87cb942602f7 100644 --- a/data/keyboards/Vendor_0957_Product_0001.kl +++ b/data/keyboards/Vendor_0957_Product_0001.kl @@ -47,7 +47,6 @@ key usage 0x00070037 PERIOD # custom keys key usage 0x000c01BB TV_INPUT -key usage 0x000c0186 MACRO_1 WAKE key usage 0x000c0185 TV_TELETEXT key usage 0x000c0061 CAPTIONS -- cgit v1.2.3-59-g8ed1b From 4b4024b5ab3a40c6658ee9466b4208f4b07425c3 Mon Sep 17 00:00:00 2001 From: Seigo Nonaka Date: Mon, 2 Oct 2023 16:12:19 +0900 Subject: Use variable definition for variable font family Bug: 281769620 Test: Manually done Test: atest FontListParserTest TypefaceSystemFallbackTest Test: atest CtsGraphicsTestCases CtsTextTestCases Change-Id: Ice5a51024c7fbba2af7c5886c751e2508c3670d7 --- core/java/android/text/FontConfig.java | 24 +- .../src/android/graphics/FontListParserTest.java | 39 +- data/fonts/font_fallback.xml | 911 ++------------------- graphics/java/android/graphics/FontListParser.java | 41 +- .../java/android/graphics/fonts/FontFamily.java | 35 +- .../java/android/graphics/fonts/SystemFonts.java | 12 +- .../server/graphics/fonts/UpdatableFontDir.java | 5 +- .../graphics/fonts/UpdatableFontDirTest.java | 10 +- 8 files changed, 228 insertions(+), 849 deletions(-) (limited to 'data') diff --git a/core/java/android/text/FontConfig.java b/core/java/android/text/FontConfig.java index cb488b08456b..c5857347fd45 100644 --- a/core/java/android/text/FontConfig.java +++ b/core/java/android/text/FontConfig.java @@ -26,6 +26,7 @@ import android.annotation.Nullable; import android.annotation.SystemApi; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; +import android.graphics.fonts.FontFamily.Builder.VariableFontFamilyType; import android.graphics.fonts.FontStyle; import android.graphics.fonts.FontVariationAxis; import android.os.Build; @@ -528,6 +529,7 @@ public final class FontConfig implements Parcelable { private final @NonNull List mFonts; private final @NonNull LocaleList mLocaleList; private final @Variant int mVariant; + private final int mVariableFontFamilyType; /** @hide */ @Retention(SOURCE) @@ -567,10 +569,11 @@ public final class FontConfig implements Parcelable { * @hide Only system server can create this instance and passed via IPC. */ public FontFamily(@NonNull List fonts, @NonNull LocaleList localeList, - @Variant int variant) { + @Variant int variant, int variableFontFamilyType) { mFonts = fonts; mLocaleList = localeList; mVariant = variant; + mVariableFontFamilyType = variableFontFamilyType; } /** @@ -621,6 +624,20 @@ public final class FontConfig implements Parcelable { return mVariant; } + /** + * Returns the font family type. + * + * @see Builder#VARIABLE_FONT_FAMILY_TYPE_NONE + * @see Builder#VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL + * @see Builder#VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY + * @see Builder#VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT + * @hide + * @return variable font family type. + */ + public @VariableFontFamilyType int getVariableFontFamilyType() { + return mVariableFontFamilyType; + } + @Override public int describeContents() { return 0; @@ -631,6 +648,7 @@ public final class FontConfig implements Parcelable { dest.writeTypedList(mFonts, flags); dest.writeString8(mLocaleList.toLanguageTags()); dest.writeInt(mVariant); + dest.writeInt(mVariableFontFamilyType); } public static final @NonNull Creator CREATOR = new Creator() { @@ -641,8 +659,10 @@ public final class FontConfig implements Parcelable { source.readTypedList(fonts, Font.CREATOR); String langTags = source.readString8(); int variant = source.readInt(); + int varFamilyType = source.readInt(); - return new FontFamily(fonts, LocaleList.forLanguageTags(langTags), variant); + return new FontFamily(fonts, LocaleList.forLanguageTags(langTags), variant, + varFamilyType); } @Override diff --git a/core/tests/coretests/src/android/graphics/FontListParserTest.java b/core/tests/coretests/src/android/graphics/FontListParserTest.java index d46f7625b596..4dd5889fdf5d 100644 --- a/core/tests/coretests/src/android/graphics/FontListParserTest.java +++ b/core/tests/coretests/src/android/graphics/FontListParserTest.java @@ -28,6 +28,7 @@ import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.fail; import android.graphics.fonts.FontCustomizationParser; +import android.graphics.fonts.FontFamily; import android.graphics.fonts.FontStyle; import android.os.LocaleList; import android.text.FontConfig; @@ -64,7 +65,8 @@ public final class FontListParserTest { Collections.singletonList(new FontConfig.FontFamily( Arrays.asList(new FontConfig.Font(new File("test.ttf"), null, "test", new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 0, "", null)), - LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT)), "sans-serif"); + LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT, + FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE)), "sans-serif"); FontConfig.NamedFamilyList family = readNamedFamily(xml); assertThat(family).isEqualTo(expected); } @@ -84,7 +86,8 @@ public final class FontListParserTest { new FontConfig.Font(new File("test.ttf"), null, "test", new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 0, "", "serif")), - LocaleList.forLanguageTags("en"), VARIANT_DEFAULT); + LocaleList.forLanguageTags("en"), VARIANT_DEFAULT, + FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE); FontConfig.FontFamily family = readFamily(xml); assertThat(family).isEqualTo(expected); @@ -101,7 +104,8 @@ public final class FontListParserTest { new FontConfig.Font(new File("test.ttf"), null, "test", new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 0, "", null)), - LocaleList.forLanguageTags("en"), VARIANT_COMPACT); + LocaleList.forLanguageTags("en"), VARIANT_COMPACT, + FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE); FontConfig.FontFamily family = readFamily(xml); assertThat(family).isEqualTo(expected); @@ -118,7 +122,8 @@ public final class FontListParserTest { new FontConfig.Font(new File("test.ttf"), null, "test", new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 0, "", null)), - LocaleList.forLanguageTags("en"), VARIANT_ELEGANT); + LocaleList.forLanguageTags("en"), VARIANT_ELEGANT, + FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE); FontConfig.FontFamily family = readFamily(xml); assertThat(family).isEqualTo(expected); @@ -140,7 +145,8 @@ public final class FontListParserTest { new FontStyle(100, FONT_SLANT_UPRIGHT), 0, "", null), new FontConfig.Font(new File("italic.ttf"), null, "test", new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_ITALIC), 0, "", null)), - LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT)), "sans-serif"); + LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT, + FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE)), "sans-serif"); FontConfig.NamedFamilyList family = readNamedFamily(xml); assertThat(family).isEqualTo(expected); } @@ -166,7 +172,8 @@ public final class FontListParserTest { new FontConfig.Font(new File("test-VF.ttf"), null, "test", new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 0, "'wdth' 400.0,'wght' 700.0", null)), - LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT)), + LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT, + FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE)), "sans-serif"); FontConfig.NamedFamilyList family = readNamedFamily(xml); assertThat(family).isEqualTo(expected); @@ -187,7 +194,8 @@ public final class FontListParserTest { new FontConfig.Font(new File("test.ttc"), null, "test", new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 1, "", null)), - LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT)), + LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT, + FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE)), "sans-serif"); FontConfig.NamedFamilyList family = readNamedFamily(xml); assertThat(family).isEqualTo(expected); @@ -206,7 +214,8 @@ public final class FontListParserTest { new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 0, "", null), new FontConfig.Font(new File("test.ttc"), null, "test", new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 1, "", null)), - LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT)), "sans-serif"); + LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT, + FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE)), "sans-serif"); FontConfig.NamedFamilyList family = readNamedFamily(xml); assertThat(family).isEqualTo(expected); } @@ -372,6 +381,20 @@ public final class FontListParserTest { .isEqualTo("emoji.ttf"); } + @Test + public void varFamilyType() throws Exception { + String xml = "" + + "" + + " " + + " test.ttf" + + " " + + ""; + FontConfig config = readFamilies(xml, true /* include non-existing font files */); + List families = config.getFontFamilies(); + assertThat(families.size()).isEqualTo(1); // legacy one should be ignored. + assertThat(families.get(0).getVariableFontFamilyType()).isEqualTo(1); + } + private FontConfig readFamilies(String xml, boolean allowNonExisting) throws IOException, XmlPullParserException { ByteArrayInputStream buffer = new ByteArrayInputStream( diff --git a/data/fonts/font_fallback.xml b/data/fonts/font_fallback.xml index 1e97fcee250d..02e032b86443 100644 --- a/data/fonts/font_fallback.xml +++ b/data/fonts/font_fallback.xml @@ -15,96 +15,9 @@ --> - - Roboto-Regular.ttf - + + Roboto-Regular.ttf - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - @@ -119,96 +32,9 @@ - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - - - - - Roboto-Regular.ttf - + + Roboto-Regular.ttf - @@ -246,13 +72,8 @@ ComingSoon.ttf - - DancingScript-Regular.ttf - - - DancingScript-Regular.ttf - - + + DancingScript-Regular.ttf @@ -269,96 +90,9 @@ - - RobotoFlex-Regular.ttf - - - - - RobotoFlex-Regular.ttf - - - - - RobotoFlex-Regular.ttf - - - - - RobotoFlex-Regular.ttf - - - - - RobotoFlex-Regular.ttf - - - - - RobotoFlex-Regular.ttf - - - - - RobotoFlex-Regular.ttf - - - - - RobotoFlex-Regular.ttf - - - - - RobotoFlex-Regular.ttf - - - - - RobotoFlex-Regular.ttf - - - - - RobotoFlex-Regular.ttf - - - - - RobotoFlex-Regular.ttf - + + RobotoFlex-Regular.ttf - - - RobotoFlex-Regular.ttf - - - - - RobotoFlex-Regular.ttf - - - - - RobotoFlex-Regular.ttf - - - - - RobotoFlex-Regular.ttf - - - - - RobotoFlex-Regular.ttf - - - - - RobotoFlex-Regular.ttf - - - @@ -375,38 +109,12 @@ NotoNaskhArabicUI-Bold.ttf - - - NotoSansEthiopic-VF.ttf - - - + + NotoSansEthiopic-VF.ttf - - - - NotoSansEthiopic-VF.ttf - - - NotoSansEthiopic-VF.ttf - - - NotoSerifEthiopic-VF.ttf - - - NotoSerifEthiopic-VF.ttf - - - NotoSerifEthiopic-VF.ttf - - - NotoSerifEthiopic-VF.ttf - + + NotoSerifEthiopic-VF.ttf @@ -432,124 +140,33 @@ NotoSansThaiUI-Bold.ttf - - - NotoSansArmenian-VF.ttf - - - - NotoSansArmenian-VF.ttf - - - - NotoSansArmenian-VF.ttf - - - + + NotoSansArmenian-VF.ttf - - NotoSerifArmenian-VF.ttf - - - NotoSerifArmenian-VF.ttf - - - NotoSerifArmenian-VF.ttf - - - NotoSerifArmenian-VF.ttf - + + NotoSerifArmenian-VF.ttf - - + + NotoSansGeorgian-VF.ttf - - - NotoSansGeorgian-VF.ttf - - - - NotoSansGeorgian-VF.ttf - - - - NotoSansGeorgian-VF.ttf - - - NotoSerifGeorgian-VF.ttf - - - NotoSerifGeorgian-VF.ttf - - - NotoSerifGeorgian-VF.ttf - - - NotoSerifGeorgian-VF.ttf - + + NotoSerifGeorgian-VF.ttf - - - NotoSansDevanagari-VF.ttf - - - - NotoSansDevanagari-VF.ttf - - - + + NotoSansDevanagari-VF.ttf - - - NotoSansDevanagari-VF.ttf - - - NotoSerifDevanagari-VF.ttf - - - NotoSerifDevanagari-VF.ttf - - - NotoSerifDevanagari-VF.ttf - - - NotoSerifDevanagari-VF.ttf - + + NotoSerifDevanagari-VF.ttf - - - NotoSansDevanagariUI-VF.ttf - - - + + NotoSansDevanagariUI-VF.ttf - - - - NotoSansDevanagariUI-VF.ttf - - - - NotoSansDevanagariUI-VF.ttf - @@ -584,316 +201,82 @@ NotoSansGujaratiUI-Bold.ttf - - + + NotoSansGurmukhi-VF.ttf - - - - NotoSansGurmukhi-VF.ttf - - - - NotoSansGurmukhi-VF.ttf - - - - NotoSansGurmukhi-VF.ttf - - - NotoSerifGurmukhi-VF.ttf - - - NotoSerifGurmukhi-VF.ttf - - - NotoSerifGurmukhi-VF.ttf - - NotoSerifGurmukhi-VF.ttf - + + NotoSerifGurmukhi-VF.ttf - - - NotoSansGurmukhiUI-VF.ttf - - - - NotoSansGurmukhiUI-VF.ttf - - - - NotoSansGurmukhiUI-VF.ttf - - - + + NotoSansGurmukhiUI-VF.ttf - - - - NotoSansTamil-VF.ttf - - - + + NotoSansTamil-VF.ttf - - - NotoSansTamil-VF.ttf - - - - NotoSansTamil-VF.ttf - - - NotoSerifTamil-VF.ttf - - - NotoSerifTamil-VF.ttf - - - NotoSerifTamil-VF.ttf - - - NotoSerifTamil-VF.ttf - + + NotoSerifTamil-VF.ttf - - - NotoSansTamilUI-VF.ttf - - - - NotoSansTamilUI-VF.ttf - - - + + NotoSansTamilUI-VF.ttf - - - - NotoSansTamilUI-VF.ttf - - - - NotoSansMalayalam-VF.ttf - - - + + NotoSansMalayalam-VF.ttf - - - NotoSansMalayalam-VF.ttf - - - - NotoSansMalayalam-VF.ttf - - - NotoSerifMalayalam-VF.ttf - - - NotoSerifMalayalam-VF.ttf - - - NotoSerifMalayalam-VF.ttf - - - NotoSerifMalayalam-VF.ttf - + + NotoSerifMalayalam-VF.ttf - - - NotoSansMalayalamUI-VF.ttf - - - - NotoSansMalayalamUI-VF.ttf - - - - NotoSansMalayalamUI-VF.ttf - - - + + NotoSansMalayalamUI-VF.ttf - - - - NotoSansBengali-VF.ttf - - - + + NotoSansBengali-VF.ttf - - - NotoSansBengali-VF.ttf - - - - NotoSansBengali-VF.ttf - - - NotoSerifBengali-VF.ttf - - - NotoSerifBengali-VF.ttf - - - NotoSerifBengali-VF.ttf - - - NotoSerifBengali-VF.ttf - + + NotoSerifBengali-VF.ttf - - - NotoSansBengaliUI-VF.ttf - - - - NotoSansBengaliUI-VF.ttf - - - - NotoSansBengaliUI-VF.ttf - - - + + NotoSansBengaliUI-VF.ttf - - - - NotoSansTelugu-VF.ttf - - - + + NotoSansTelugu-VF.ttf - - - - NotoSansTelugu-VF.ttf - - - - NotoSansTelugu-VF.ttf - - - NotoSerifTelugu-VF.ttf - - - NotoSerifTelugu-VF.ttf - - NotoSerifTelugu-VF.ttf - - - NotoSerifTelugu-VF.ttf - + + NotoSerifTelugu-VF.ttf - - - NotoSansTeluguUI-VF.ttf - - - - NotoSansTeluguUI-VF.ttf - - - + + NotoSansTeluguUI-VF.ttf - - - - NotoSansTeluguUI-VF.ttf - - - + + NotoSansKannada-VF.ttf - - - - NotoSansKannada-VF.ttf - - - - NotoSansKannada-VF.ttf - - - - NotoSansKannada-VF.ttf - - - NotoSerifKannada-VF.ttf - - - NotoSerifKannada-VF.ttf - - - NotoSerifKannada-VF.ttf - - NotoSerifKannada-VF.ttf - + + NotoSerifKannada-VF.ttf - - - NotoSansKannadaUI-VF.ttf - - - + + NotoSansKannadaUI-VF.ttf - - - - NotoSansKannadaUI-VF.ttf - - - - NotoSansKannadaUI-VF.ttf - @@ -907,56 +290,17 @@ NotoSansOriyaUI-Bold.ttf - - + + NotoSansSinhala-VF.ttf - - - NotoSansSinhala-VF.ttf - - - - NotoSansSinhala-VF.ttf - - - - NotoSansSinhala-VF.ttf - - - NotoSerifSinhala-VF.ttf - - - NotoSerifSinhala-VF.ttf - - - NotoSerifSinhala-VF.ttf - - - NotoSerifSinhala-VF.ttf - + + NotoSerifSinhala-VF.ttf - - - NotoSansSinhalaUI-VF.ttf - - - - NotoSansSinhalaUI-VF.ttf - - - - NotoSansSinhalaUI-VF.ttf - - - + + NotoSansSinhalaUI-VF.ttf - @@ -1054,22 +398,9 @@ NotoSansAhom-Regular.otf - - - NotoSansAdlam-VF.ttf - - - + + NotoSansAdlam-VF.ttf - - - - NotoSansAdlam-VF.ttf - - - - NotoSansAdlam-VF.ttf - @@ -1355,22 +686,9 @@ NotoSansTaiViet-Regular.ttf - - + + NotoSerifTibetan-VF.ttf - - - - NotoSerifTibetan-VF.ttf - - - - NotoSerifTibetan-VF.ttf - - - - NotoSerifTibetan-VF.ttf - @@ -1537,94 +855,29 @@ NotoSerifDogra-Regular.ttf - - - NotoSansMedefaidrin-VF.ttf - - - - NotoSansMedefaidrin-VF.ttf - - - - NotoSansMedefaidrin-VF.ttf - - - + + NotoSansMedefaidrin-VF.ttf - - - - NotoSansSoyombo-VF.ttf - - - + + NotoSansSoyombo-VF.ttf - - - - NotoSansSoyombo-VF.ttf - - - - NotoSansSoyombo-VF.ttf - - - + + NotoSansTakri-VF.ttf - - - - NotoSansTakri-VF.ttf - - - - NotoSansTakri-VF.ttf - - - - NotoSansTakri-VF.ttf - - - - NotoSerifNyiakengPuachueHmong-VF.ttf - - - - NotoSerifNyiakengPuachueHmong-VF.ttf - - - - NotoSerifNyiakengPuachueHmong-VF.ttf - - - + + NotoSerifNyiakengPuachueHmong-VF.ttf - - - - NotoSerifYezidi-VF.ttf - - - + + NotoSerifYezidi-VF.ttf - - - - NotoSerifYezidi-VF.ttf - - - - NotoSerifYezidi-VF.ttf - diff --git a/graphics/java/android/graphics/FontListParser.java b/graphics/java/android/graphics/FontListParser.java index 674246acafef..735bc180c015 100644 --- a/graphics/java/android/graphics/FontListParser.java +++ b/graphics/java/android/graphics/FontListParser.java @@ -16,6 +16,10 @@ package android.graphics; +import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE; +import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL; +import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY; +import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT; import static android.text.FontConfig.NamedFamilyList; import android.annotation.NonNull; @@ -28,6 +32,7 @@ import android.os.Build; import android.os.LocaleList; import android.text.FontConfig; import android.util.ArraySet; +import android.util.Log; import android.util.Xml; import org.xmlpull.v1.XmlPullParser; @@ -256,6 +261,7 @@ public class FontListParser { final String lang = parser.getAttributeValue("", "lang"); final String variant = parser.getAttributeValue(null, "variant"); final String ignore = parser.getAttributeValue(null, "ignore"); + final String varFamilyTypeStr = parser.getAttributeValue(null, "varFamilyType"); final List fonts = new ArrayList<>(); while (keepReading(parser)) { if (parser.getEventType() != XmlPullParser.START_TAG) continue; @@ -278,12 +284,45 @@ public class FontListParser { intVariant = FontConfig.FontFamily.VARIANT_ELEGANT; } } + int varFamilyType = VARIABLE_FONT_FAMILY_TYPE_NONE; + if (varFamilyTypeStr != null) { + varFamilyType = Integer.parseInt(varFamilyTypeStr); + if (varFamilyType <= -1 || varFamilyType > 3) { + Log.e(TAG, "Error: unexpected varFamilyType value: " + varFamilyTypeStr); + varFamilyType = VARIABLE_FONT_FAMILY_TYPE_NONE; + } + + // validation but don't read font content for performance reasons. + switch (varFamilyType) { + case VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY: + if (fonts.size() != 1) { + Log.e(TAG, "Error: Single font support wght axis, but two or more fonts are" + + " included in the font family."); + varFamilyType = VARIABLE_FONT_FAMILY_TYPE_NONE; + } + break; + case VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL: + if (fonts.size() != 1) { + Log.e(TAG, "Error: Single font support both ital and wght axes, but two or" + + " more fonts are included in the font family."); + varFamilyType = VARIABLE_FONT_FAMILY_TYPE_NONE; + } + break; + case VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT: + if (fonts.size() != 2) { + Log.e(TAG, "Error: two fonts that support wght axis, but one or three or" + + " more fonts are included in the font family."); + varFamilyType = VARIABLE_FONT_FAMILY_TYPE_NONE; + } + } + } boolean skip = (ignore != null && (ignore.equals("true") || ignore.equals("1"))); if (skip || fonts.isEmpty()) { return null; } - return new FontConfig.FontFamily(fonts, LocaleList.forLanguageTags(lang), intVariant); + return new FontConfig.FontFamily(fonts, LocaleList.forLanguageTags(lang), intVariant, + varFamilyType); } private static void throwIfAttributeExists(String attrName, XmlPullParser parser) { diff --git a/graphics/java/android/graphics/fonts/FontFamily.java b/graphics/java/android/graphics/fonts/FontFamily.java index 5e4110590325..4c753565eb5b 100644 --- a/graphics/java/android/graphics/fonts/FontFamily.java +++ b/graphics/java/android/graphics/fonts/FontFamily.java @@ -18,7 +18,10 @@ package android.graphics.fonts; import static com.android.text.flags.Flags.FLAG_DEPRECATE_FONTS_XML; +import static java.lang.annotation.RetentionPolicy.SOURCE; + import android.annotation.FlaggedApi; +import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; @@ -32,6 +35,7 @@ import dalvik.annotation.optimization.FastNative; import libcore.util.NativeAllocationRegistry; +import java.lang.annotation.Retention; import java.util.ArrayList; import java.util.Set; @@ -184,32 +188,59 @@ public final class FontFamily { } /** + * A special variable font family type that indicates `analyzeAndResolveVariableType` could + * not be identified the variable font family type. + * * @see #buildVariableFamily() * @hide */ public static final int VARIABLE_FONT_FAMILY_TYPE_UNKNOWN = -1; /** + * A variable font family type that indicates no variable font family can be used. + * + * The font family is used as bundle of static fonts. * @see #buildVariableFamily() * @hide */ public static final int VARIABLE_FONT_FAMILY_TYPE_NONE = 0; /** + * A variable font family type that indicates single font file can be used for multiple + * weight. For the italic style, fake italic may be applied. + * * @see #buildVariableFamily() * @hide */ public static final int VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY = 1; /** + * A variable font family type that indicates single font file can be used for multiple + * weight and italic. + * * @see #buildVariableFamily() * @hide */ public static final int VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL = 2; /** + * A variable font family type that indicates two font files are included in the family: + * one can be used for upright with various weights, the other one can be used for italic + * with various weights. + * * @see #buildVariableFamily() * @hide */ public static final int VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT = 3; + /** @hide */ + @Retention(SOURCE) + @IntDef(prefix = { "VARIABLE_FONT_FAMILY_TYPE_" }, value = { + VARIABLE_FONT_FAMILY_TYPE_UNKNOWN, + VARIABLE_FONT_FAMILY_TYPE_NONE, + VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY, + VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL, + VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT + }) + public @interface VariableFontFamilyType {} + /** * The registered italic axis used for adjusting requested style. * https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxistag_ital @@ -222,7 +253,9 @@ public final class FontFamily { */ private static final int TAG_wght = 0x77676874; // w(0x77), g(0x67), h(0x68), t(0x74) - private static int analyzeAndResolveVariableType(ArrayList fonts) { + /** @hide */ + public static @VariableFontFamilyType int analyzeAndResolveVariableType( + ArrayList fonts) { if (fonts.size() > 2) { return VARIABLE_FONT_FAMILY_TYPE_UNKNOWN; } diff --git a/graphics/java/android/graphics/fonts/SystemFonts.java b/graphics/java/android/graphics/fonts/SystemFonts.java index 9810022abfed..d4e35b30c8d0 100644 --- a/graphics/java/android/graphics/fonts/SystemFonts.java +++ b/graphics/java/android/graphics/fonts/SystemFonts.java @@ -121,7 +121,8 @@ public final class SystemFonts { final FontFamily defaultFamily = defaultFonts.isEmpty() ? null : createFontFamily( - defaultFonts, languageTags, variant, false, cache); + defaultFonts, languageTags, variant, xmlFamily.getVariableFontFamilyType(), false, + cache); // Insert family into fallback map. for (int i = 0; i < fallbackMap.size(); i++) { final String name = fallbackMap.keyAt(i); @@ -138,8 +139,8 @@ public final class SystemFonts { familyListSet.familyList.add(defaultFamily); } } else { - final FontFamily family = createFontFamily(fallback, languageTags, variant, false, - cache); + final FontFamily family = createFontFamily(fallback, languageTags, variant, + xmlFamily.getVariableFontFamilyType(), false, cache); if (family != null) { familyListSet.familyList.add(family); } else if (defaultFamily != null) { @@ -155,6 +156,7 @@ public final class SystemFonts { @NonNull List fonts, @NonNull String languageTags, @FontConfig.FontFamily.Variant int variant, + int varFamilyType, boolean isDefaultFallback, @NonNull Map cache) { if (fonts.size() == 0) { @@ -196,7 +198,7 @@ public final class SystemFonts { } } return b == null ? null : b.build(languageTags, variant, false /* isCustomFallback */, - isDefaultFallback, FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE); + isDefaultFallback, varFamilyType); } private static void appendNamedFamilyList(@NonNull FontConfig.NamedFamilyList namedFamilyList, @@ -210,6 +212,7 @@ public final class SystemFonts { final FontFamily family = createFontFamily( xmlFamily.getFontList(), xmlFamily.getLocaleList().toLanguageTags(), xmlFamily.getVariant(), + xmlFamily.getVariableFontFamilyType(), true, // named family is always default bufferCache); if (family == null) { @@ -291,6 +294,7 @@ public final class SystemFonts { int configVersion ) { try { + Log.i(TAG, "Loading font config from " + fontsXml); return FontListParser.parse(fontsXml, systemFontDir, oemXml, productFontDir, updatableFontMap, lastModifiedDate, configVersion); } catch (IOException e) { diff --git a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java index a680f5001479..cd3d2f031455 100644 --- a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java +++ b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java @@ -16,6 +16,8 @@ package com.android.server.graphics.fonts; +import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE; + import static com.android.server.graphics.fonts.FontManagerService.SystemFontException; import android.annotation.NonNull; @@ -581,7 +583,8 @@ final class UpdatableFontDir { font.getFontStyle(), font.getIndex(), font.getFontVariationSettings(), null)); } FontConfig.FontFamily family = new FontConfig.FontFamily(resolvedFonts, - LocaleList.getEmptyLocaleList(), FontConfig.FontFamily.VARIANT_DEFAULT); + LocaleList.getEmptyLocaleList(), FontConfig.FontFamily.VARIANT_DEFAULT, + VARIABLE_FONT_FAMILY_TYPE_NONE); return new FontConfig.NamedFamilyList(Collections.singletonList(family), fontFamily.getName()); } diff --git a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java index 037637630b7a..184c976b86bf 100644 --- a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java +++ b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java @@ -23,6 +23,7 @@ import static org.junit.Assert.fail; import android.content.Context; import android.graphics.FontListParser; +import android.graphics.fonts.FontFamily; import android.graphics.fonts.FontManager; import android.graphics.fonts.FontStyle; import android.graphics.fonts.FontUpdateRequest; @@ -330,7 +331,8 @@ public final class UpdatableFontDirTest { FontConfig.FontFamily family = new FontConfig.FontFamily( Arrays.asList(fooFont, barFont), null, - FontConfig.FontFamily.VARIANT_DEFAULT); + FontConfig.FontFamily.VARIANT_DEFAULT, + FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE); return new FontConfig(Collections.emptyList(), Collections.emptyList(), Collections.singletonList(new FontConfig.NamedFamilyList( @@ -491,7 +493,8 @@ public final class UpdatableFontDirTest { file, null, "bar", new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null, null); FontConfig.FontFamily family = new FontConfig.FontFamily( - Collections.singletonList(font), null, FontConfig.FontFamily.VARIANT_DEFAULT); + Collections.singletonList(font), null, FontConfig.FontFamily.VARIANT_DEFAULT, + FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE); return new FontConfig( Collections.emptyList(), Collections.emptyList(), @@ -644,7 +647,8 @@ public final class UpdatableFontDirTest { file, null, "test", new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null, null); FontConfig.FontFamily family = new FontConfig.FontFamily( - Collections.singletonList(font), null, FontConfig.FontFamily.VARIANT_DEFAULT); + Collections.singletonList(font), null, FontConfig.FontFamily.VARIANT_DEFAULT, + FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE); return new FontConfig(Collections.emptyList(), Collections.emptyList(), Collections.singletonList(new FontConfig.NamedFamilyList( Collections.singletonList(family), "sans-serif")), 0, 1); -- cgit v1.2.3-59-g8ed1b From 6321bda515c7e2d42d8358b4d977208229190596 Mon Sep 17 00:00:00 2001 From: Marzia Favaro Date: Tue, 29 Aug 2023 14:56:22 +0000 Subject: Apply dim changes only at the end of traversal Collect the changes on a dim layer and assign only the last requested to a transaction - Avoids issues with transaction ending up in the wrong order - Allows smooth transactions between different alpha values - Does not treat the entering animation as a special animation case Test: DimmerTests Bug: 281632483 Bug: 295291019 Change-Id: Ic3e6ce1f107bfc3ac925eb167b8bc89a5df478c8 --- .../window/flags/windowing_frontend.aconfig | 7 + .../android/internal/protolog/ProtoLogGroup.java | 2 + data/etc/services.core.protolog.json | 21 ++ .../com/android/server/wm/AnimationAdapter.java | 4 +- .../core/java/com/android/server/wm/Dimmer.java | 331 ++--------------- .../java/com/android/server/wm/DisplayArea.java | 3 +- .../java/com/android/server/wm/LegacyDimmer.java | 341 ++++++++++++++++++ .../java/com/android/server/wm/SmoothDimmer.java | 395 +++++++++++++++++++++ .../com/android/server/wm/SurfaceAnimator.java | 6 +- .../java/com/android/server/wm/TaskFragment.java | 4 +- .../src/com/android/server/wm/DimmerTests.java | 208 +++++++---- .../android/server/wm/utils/LastCallVerifier.java | 68 ++++ .../server/wm/utils/MockAnimationAdapter.java | 64 ++++ 13 files changed, 1075 insertions(+), 379 deletions(-) create mode 100644 services/core/java/com/android/server/wm/LegacyDimmer.java create mode 100644 services/core/java/com/android/server/wm/SmoothDimmer.java create mode 100644 services/tests/wmtests/src/com/android/server/wm/utils/LastCallVerifier.java create mode 100644 services/tests/wmtests/src/com/android/server/wm/utils/MockAnimationAdapter.java (limited to 'data') diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 7c931cd9fa15..9caf87f32b8e 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -13,3 +13,10 @@ flag { description: "On close to square display, when necessary, configuration includes status bar" bug: "291870756" } + +flag { + name: "dimmer_refactor" + namespace: "windowing_frontend" + description: "Refactor dim to fix flickers" + bug: "281632483,295291019" +} \ No newline at end of file diff --git a/core/java/com/android/internal/protolog/ProtoLogGroup.java b/core/java/com/android/internal/protolog/ProtoLogGroup.java index ec525f09fa88..4bb7c33b41e2 100644 --- a/core/java/com/android/internal/protolog/ProtoLogGroup.java +++ b/core/java/com/android/internal/protolog/ProtoLogGroup.java @@ -91,6 +91,8 @@ public enum ProtoLogGroup implements IProtoLogGroup { WM_DEBUG_BACK_PREVIEW(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, "CoreBackPreview"), WM_DEBUG_DREAM(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, Consts.TAG_WM), + + WM_DEBUG_DIMMER(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM), TEST_GROUP(true, true, false, "WindowManagerProtoLogTest"); private final boolean mEnabled; diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 9c6528785584..b71aaf3fc2e6 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -1801,6 +1801,12 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "-504637678": { + "message": "Starting animation on dim layer %s, requested by %s, alpha: %f -> %f, blur: %d -> %d", + "level": "VERBOSE", + "group": "WM_DEBUG_DIMMER", + "at": "com\/android\/server\/wm\/SmoothDimmer.java" + }, "-503656156": { "message": "Update process config of %s to new config %s", "level": "VERBOSE", @@ -3739,6 +3745,12 @@ "group": "WM_DEBUG_CONFIGURATION", "at": "com\/android\/server\/wm\/ActivityClientController.java" }, + "1309365288": { + "message": "Removing dim surface %s on transaction %s", + "level": "DEBUG", + "group": "WM_DEBUG_DIMMER", + "at": "com\/android\/server\/wm\/SmoothDimmer.java" + }, "1316533291": { "message": "State movement: %s from:%s to:%s reason:%s", "level": "VERBOSE", @@ -4003,6 +4015,12 @@ "group": "WM_DEBUG_STATES", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "1620751818": { + "message": "Dim %s skipping animation and directly setting alpha=%f, blur=%d", + "level": "DEBUG", + "group": "WM_DEBUG_DIMMER", + "at": "com\/android\/server\/wm\/SmoothDimmer.java" + }, "1621562070": { "message": " startWCT=%s", "level": "VERBOSE", @@ -4560,6 +4578,9 @@ "WM_DEBUG_CONTENT_RECORDING": { "tag": "WindowManager" }, + "WM_DEBUG_DIMMER": { + "tag": "WindowManager" + }, "WM_DEBUG_DRAW": { "tag": "WindowManager" }, diff --git a/services/core/java/com/android/server/wm/AnimationAdapter.java b/services/core/java/com/android/server/wm/AnimationAdapter.java index b039646c1697..3dc377dbc14c 100644 --- a/services/core/java/com/android/server/wm/AnimationAdapter.java +++ b/services/core/java/com/android/server/wm/AnimationAdapter.java @@ -22,6 +22,7 @@ import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; import android.view.animation.Animation; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.wm.SurfaceAnimator.AnimationType; import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback; @@ -31,7 +32,8 @@ import java.io.PrintWriter; * Interface that describes an animation and bridges the animation start to the component * responsible for running the animation. */ -interface AnimationAdapter { +@VisibleForTesting +public interface AnimationAdapter { long STATUS_BAR_TRANSITION_DURATION = 120L; diff --git a/services/core/java/com/android/server/wm/Dimmer.java b/services/core/java/com/android/server/wm/Dimmer.java index ae29afa9fc49..64a230effb38 100644 --- a/services/core/java/com/android/server/wm/Dimmer.java +++ b/services/core/java/com/android/server/wm/Dimmer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -11,172 +11,36 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License + * limitations under the License. */ package com.android.server.wm; -import static com.android.server.wm.AlphaAnimationSpecProto.DURATION_MS; -import static com.android.server.wm.AlphaAnimationSpecProto.FROM; -import static com.android.server.wm.AlphaAnimationSpecProto.TO; -import static com.android.server.wm.AnimationSpecProto.ALPHA; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_DIMMER; - import android.annotation.NonNull; import android.graphics.Rect; -import android.util.Log; -import android.util.proto.ProtoOutputStream; -import android.view.Surface; import android.view.SurfaceControl; import com.android.internal.annotations.VisibleForTesting; -import com.android.server.wm.SurfaceAnimator.AnimationType; - -import java.io.PrintWriter; +import com.android.window.flags.Flags; /** * Utility class for use by a WindowContainer implementation to add "DimLayer" support, that is * black layers of varying opacity at various Z-levels which create the effect of a Dim. */ -class Dimmer { - private static final String TAG = "WindowManager"; - // This is in milliseconds. - private static final int DEFAULT_DIM_ANIM_DURATION = 200; - - private class DimAnimatable implements SurfaceAnimator.Animatable { - private SurfaceControl mDimLayer; - - private DimAnimatable(SurfaceControl dimLayer) { - mDimLayer = dimLayer; - } - - @Override - public SurfaceControl.Transaction getSyncTransaction() { - return mHost.getSyncTransaction(); - } - - @Override - public SurfaceControl.Transaction getPendingTransaction() { - return mHost.getPendingTransaction(); - } - - @Override - public void commitPendingTransaction() { - mHost.commitPendingTransaction(); - } - - @Override - public void onAnimationLeashCreated(SurfaceControl.Transaction t, SurfaceControl leash) { - } - - @Override - public void onAnimationLeashLost(SurfaceControl.Transaction t) { - } - - @Override - public SurfaceControl.Builder makeAnimationLeash() { - return mHost.makeAnimationLeash(); - } - - @Override - public SurfaceControl getAnimationLeashParent() { - return mHost.getSurfaceControl(); - } - - @Override - public SurfaceControl getSurfaceControl() { - return mDimLayer; - } - - @Override - public SurfaceControl getParentSurfaceControl() { - return mHost.getSurfaceControl(); - } - - @Override - public int getSurfaceWidth() { - // This will determine the size of the leash created. This should be the size of the - // host and not the dim layer since the dim layer may get bigger during animation. If - // that occurs, the leash size cannot change so we need to ensure the leash is big - // enough that the dim layer can grow. - // This works because the mHost will be a Task which has the display bounds. - return mHost.getSurfaceWidth(); - } - - @Override - public int getSurfaceHeight() { - // See getSurfaceWidth() above for explanation. - return mHost.getSurfaceHeight(); - } - - void removeSurface() { - if (mDimLayer != null && mDimLayer.isValid()) { - getSyncTransaction().remove(mDimLayer); - } - mDimLayer = null; - } - } - - @VisibleForTesting - class DimState { - /** - * The layer where property changes should be invoked on. - */ - SurfaceControl mDimLayer; - boolean mDimming; - boolean isVisible; - SurfaceAnimator mSurfaceAnimator; - - // TODO(b/64816140): Remove after confirming dimmer layer always matches its container. - final Rect mDimBounds = new Rect(); - - /** - * Determines whether the dim layer should animate before destroying. - */ - boolean mAnimateExit = true; - - /** - * Used for Dims not associated with a WindowContainer. See {@link Dimmer#dimAbove} for - * details on Dim lifecycle. - */ - boolean mDontReset; - - DimState(SurfaceControl dimLayer) { - mDimLayer = dimLayer; - mDimming = true; - final DimAnimatable dimAnimatable = new DimAnimatable(dimLayer); - mSurfaceAnimator = new SurfaceAnimator(dimAnimatable, (type, anim) -> { - if (!mDimming) { - dimAnimatable.removeSurface(); - } - }, mHost.mWmService); - } - } - +public abstract class Dimmer { /** - * The {@link WindowContainer} that our Dim's are bounded to. We may be dimming on behalf of the + * The {@link WindowContainer} that our Dims are bounded to. We may be dimming on behalf of the * host, some controller of it, or one of the hosts children. */ - private WindowContainer mHost; - private WindowContainer mLastRequestedDimContainer; - @VisibleForTesting - DimState mDimState; - - private final SurfaceAnimatorStarter mSurfaceAnimatorStarter; - - @VisibleForTesting - interface SurfaceAnimatorStarter { - void startAnimation(SurfaceAnimator surfaceAnimator, SurfaceControl.Transaction t, - AnimationAdapter anim, boolean hidden, @AnimationType int type); - } + protected final WindowContainer mHost; - Dimmer(WindowContainer host) { - this(host, SurfaceAnimator::startAnimation); + protected Dimmer(WindowContainer host) { + mHost = host; } - Dimmer(WindowContainer host, SurfaceAnimatorStarter surfaceAnimatorStarter) { - mHost = host; - mSurfaceAnimatorStarter = surfaceAnimatorStarter; + // Constructs the correct type of dimmer + static Dimmer create(WindowContainer host) { + return Flags.dimmerRefactor() ? new SmoothDimmer(host) : new LegacyDimmer(host); } @NonNull @@ -184,49 +48,8 @@ class Dimmer { return mHost; } - private SurfaceControl makeDimLayer() { - return mHost.makeChildSurface(null) - .setParent(mHost.getSurfaceControl()) - .setColorLayer() - .setName("Dim Layer for - " + mHost.getName()) - .setCallsite("Dimmer.makeDimLayer") - .build(); - } - - /** - * Retrieve the DimState, creating one if it doesn't exist. - */ - private DimState getDimState(WindowContainer container) { - if (mDimState == null) { - try { - final SurfaceControl ctl = makeDimLayer(); - mDimState = new DimState(ctl); - } catch (Surface.OutOfResourcesException e) { - Log.w(TAG, "OutOfResourcesException creating dim surface"); - } - } - - mLastRequestedDimContainer = container; - return mDimState; - } - - private void dim(WindowContainer container, int relativeLayer, float alpha, int blurRadius) { - final DimState d = getDimState(container); - - if (d == null) { - return; - } - - // The dim method is called from WindowState.prepareSurfaces(), which is always called - // in the correct Z from lowest Z to highest. This ensures that the dim layer is always - // relative to the highest Z layer with a dim. - SurfaceControl.Transaction t = mHost.getPendingTransaction(); - t.setRelativeLayer(d.mDimLayer, container.getSurfaceControl(), relativeLayer); - t.setAlpha(d.mDimLayer, alpha); - t.setBackgroundBlurRadius(d.mDimLayer, blurRadius); - - d.mDimming = true; - } + protected abstract void dim( + WindowContainer container, int relativeLayer, float alpha, int blurRadius); /** * Place a dim above the given container, which should be a child of the host container. @@ -260,25 +83,15 @@ class Dimmer { * chain {@link WindowContainer#prepareSurfaces} down to it's children to give them * a chance to request dims to continue. */ - void resetDimStates() { - if (mDimState == null) { - return; - } - if (!mDimState.mDontReset) { - mDimState.mDimming = false; - } - } + abstract void resetDimStates(); /** Returns non-null bounds if the dimmer is showing. */ - Rect getDimBounds() { - return mDimState != null ? mDimState.mDimBounds : null; - } + abstract Rect getDimBounds(); - void dontAnimateExit() { - if (mDimState != null) { - mDimState.mAnimateExit = false; - } - } + abstract void dontAnimateExit(); + + @VisibleForTesting + abstract SurfaceControl getDimLayer(); /** * Call after invoking {@link WindowContainer#prepareSurfaces} on children as @@ -288,109 +101,5 @@ class Dimmer { * @param t A transaction in which to update the dims. * @return true if any Dims were updated. */ - boolean updateDims(SurfaceControl.Transaction t) { - if (mDimState == null) { - return false; - } - - if (!mDimState.mDimming) { - if (!mDimState.mAnimateExit) { - if (mDimState.mDimLayer.isValid()) { - t.remove(mDimState.mDimLayer); - } - } else { - startDimExit(mLastRequestedDimContainer, mDimState.mSurfaceAnimator, t); - } - mDimState = null; - return false; - } else { - final Rect bounds = mDimState.mDimBounds; - // TODO: Once we use geometry from hierarchy this falls away. - t.setPosition(mDimState.mDimLayer, bounds.left, bounds.top); - t.setWindowCrop(mDimState.mDimLayer, bounds.width(), bounds.height()); - if (!mDimState.isVisible) { - mDimState.isVisible = true; - t.show(mDimState.mDimLayer); - // Skip enter animation while starting window is on top of its activity - final WindowState ws = mLastRequestedDimContainer.asWindowState(); - if (ws == null || ws.mActivityRecord == null - || ws.mActivityRecord.mStartingData == null) { - startDimEnter(mLastRequestedDimContainer, mDimState.mSurfaceAnimator, t); - } - } - return true; - } - } - - private void startDimEnter(WindowContainer container, SurfaceAnimator animator, - SurfaceControl.Transaction t) { - startAnim(container, animator, t, 0 /* startAlpha */, 1 /* endAlpha */); - } - - private void startDimExit(WindowContainer container, SurfaceAnimator animator, - SurfaceControl.Transaction t) { - startAnim(container, animator, t, 1 /* startAlpha */, 0 /* endAlpha */); - } - - private void startAnim(WindowContainer container, SurfaceAnimator animator, - SurfaceControl.Transaction t, float startAlpha, float endAlpha) { - mSurfaceAnimatorStarter.startAnimation(animator, t, new LocalAnimationAdapter( - new AlphaAnimationSpec(startAlpha, endAlpha, getDimDuration(container)), - mHost.mWmService.mSurfaceAnimationRunner), false /* hidden */, - ANIMATION_TYPE_DIMMER); - } - - private long getDimDuration(WindowContainer container) { - // If there's no container, then there isn't an animation occurring while dimming. Set the - // duration to 0 so it immediately dims to the set alpha. - if (container == null) { - return 0; - } - - // Otherwise use the same duration as the animation on the WindowContainer - AnimationAdapter animationAdapter = container.mSurfaceAnimator.getAnimation(); - final float durationScale = container.mWmService.getTransitionAnimationScaleLocked(); - return animationAdapter == null ? (long) (DEFAULT_DIM_ANIM_DURATION * durationScale) - : animationAdapter.getDurationHint(); - } - - private static class AlphaAnimationSpec implements LocalAnimationAdapter.AnimationSpec { - private final long mDuration; - private final float mFromAlpha; - private final float mToAlpha; - - AlphaAnimationSpec(float fromAlpha, float toAlpha, long duration) { - mFromAlpha = fromAlpha; - mToAlpha = toAlpha; - mDuration = duration; - } - - @Override - public long getDuration() { - return mDuration; - } - - @Override - public void apply(SurfaceControl.Transaction t, SurfaceControl sc, long currentPlayTime) { - final float fraction = getFraction(currentPlayTime); - final float alpha = fraction * (mToAlpha - mFromAlpha) + mFromAlpha; - t.setAlpha(sc, alpha); - } - - @Override - public void dump(PrintWriter pw, String prefix) { - pw.print(prefix); pw.print("from="); pw.print(mFromAlpha); - pw.print(" to="); pw.print(mToAlpha); - pw.print(" duration="); pw.println(mDuration); - } - - @Override - public void dumpDebugInner(ProtoOutputStream proto) { - final long token = proto.start(ALPHA); - proto.write(FROM, mFromAlpha); - proto.write(TO, mToAlpha); - proto.write(DURATION_MS, mDuration); - proto.end(token); - } - } + abstract boolean updateDims(SurfaceControl.Transaction t); } diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java index f81e5d453434..795b247e3876 100644 --- a/services/core/java/com/android/server/wm/DisplayArea.java +++ b/services/core/java/com/android/server/wm/DisplayArea.java @@ -53,7 +53,6 @@ import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; - /** * Container for grouping WindowContainer below DisplayContent. * @@ -766,7 +765,7 @@ public class DisplayArea extends WindowContainer { * DisplayArea that can be dimmed. */ static class Dimmable extends DisplayArea { - private final Dimmer mDimmer = new Dimmer(this); + private final Dimmer mDimmer = Dimmer.create(this); Dimmable(WindowManagerService wms, Type type, String name, int featureId) { super(wms, type, name, featureId); diff --git a/services/core/java/com/android/server/wm/LegacyDimmer.java b/services/core/java/com/android/server/wm/LegacyDimmer.java new file mode 100644 index 000000000000..ccf956ecef1e --- /dev/null +++ b/services/core/java/com/android/server/wm/LegacyDimmer.java @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static com.android.server.wm.AlphaAnimationSpecProto.DURATION_MS; +import static com.android.server.wm.AlphaAnimationSpecProto.FROM; +import static com.android.server.wm.AlphaAnimationSpecProto.TO; +import static com.android.server.wm.AnimationSpecProto.ALPHA; +import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_DIMMER; +import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; + +import android.graphics.Rect; +import android.util.Log; +import android.util.proto.ProtoOutputStream; +import android.view.Surface; +import android.view.SurfaceControl; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.PrintWriter; + +public class LegacyDimmer extends Dimmer { + private static final String TAG = TAG_WITH_CLASS_NAME ? "Dimmer" : TAG_WM; + // This is in milliseconds. + private static final int DEFAULT_DIM_ANIM_DURATION = 200; + DimState mDimState; + private WindowContainer mLastRequestedDimContainer; + private final SurfaceAnimatorStarter mSurfaceAnimatorStarter; + + private class DimAnimatable implements SurfaceAnimator.Animatable { + private SurfaceControl mDimLayer; + + private DimAnimatable(SurfaceControl dimLayer) { + mDimLayer = dimLayer; + } + + @Override + public SurfaceControl.Transaction getSyncTransaction() { + return mHost.getSyncTransaction(); + } + + @Override + public SurfaceControl.Transaction getPendingTransaction() { + return mHost.getPendingTransaction(); + } + + @Override + public void commitPendingTransaction() { + mHost.commitPendingTransaction(); + } + + @Override + public void onAnimationLeashCreated(SurfaceControl.Transaction t, SurfaceControl leash) { + } + + @Override + public void onAnimationLeashLost(SurfaceControl.Transaction t) { + } + + @Override + public SurfaceControl.Builder makeAnimationLeash() { + return mHost.makeAnimationLeash(); + } + + @Override + public SurfaceControl getAnimationLeashParent() { + return mHost.getSurfaceControl(); + } + + @Override + public SurfaceControl getSurfaceControl() { + return mDimLayer; + } + + @Override + public SurfaceControl getParentSurfaceControl() { + return mHost.getSurfaceControl(); + } + + @Override + public int getSurfaceWidth() { + // This will determine the size of the leash created. This should be the size of the + // host and not the dim layer since the dim layer may get bigger during animation. If + // that occurs, the leash size cannot change so we need to ensure the leash is big + // enough that the dim layer can grow. + // This works because the mHost will be a Task which has the display bounds. + return mHost.getSurfaceWidth(); + } + + @Override + public int getSurfaceHeight() { + // See getSurfaceWidth() above for explanation. + return mHost.getSurfaceHeight(); + } + + void removeSurface() { + if (mDimLayer != null && mDimLayer.isValid()) { + getSyncTransaction().remove(mDimLayer); + } + mDimLayer = null; + } + } + + @VisibleForTesting + class DimState { + /** + * The layer where property changes should be invoked on. + */ + SurfaceControl mDimLayer; + boolean mDimming; + boolean mIsVisible; + + // TODO(b/64816140): Remove after confirming dimmer layer always matches its container. + final Rect mDimBounds = new Rect(); + + /** + * Determines whether the dim layer should animate before destroying. + */ + boolean mAnimateExit = true; + + /** + * Used for Dims not associated with a WindowContainer. See {@link Dimmer#dimAbove} for + * details on Dim lifecycle. + */ + boolean mDontReset; + SurfaceAnimator mSurfaceAnimator; + + DimState(SurfaceControl dimLayer) { + mDimLayer = dimLayer; + mDimming = true; + final DimAnimatable dimAnimatable = new DimAnimatable(dimLayer); + mSurfaceAnimator = new SurfaceAnimator(dimAnimatable, (type, anim) -> { + if (!mDimming) { + dimAnimatable.removeSurface(); + } + }, mHost.mWmService); + } + } + + @VisibleForTesting + interface SurfaceAnimatorStarter { + void startAnimation(SurfaceAnimator surfaceAnimator, SurfaceControl.Transaction t, + AnimationAdapter anim, boolean hidden, @SurfaceAnimator.AnimationType int type); + } + + protected LegacyDimmer(WindowContainer host) { + this(host, SurfaceAnimator::startAnimation); + } + + LegacyDimmer(WindowContainer host, SurfaceAnimatorStarter surfaceAnimatorStarter) { + super(host); + mSurfaceAnimatorStarter = surfaceAnimatorStarter; + } + + private DimState obtainDimState(WindowContainer container) { + if (mDimState == null) { + try { + final SurfaceControl ctl = makeDimLayer(); + mDimState = new DimState(ctl); + } catch (Surface.OutOfResourcesException e) { + Log.w(TAG, "OutOfResourcesException creating dim surface"); + } + } + + mLastRequestedDimContainer = container; + return mDimState; + } + + private SurfaceControl makeDimLayer() { + return mHost.makeChildSurface(null) + .setParent(mHost.getSurfaceControl()) + .setColorLayer() + .setName("Dim Layer for - " + mHost.getName()) + .setCallsite("Dimmer.makeDimLayer") + .build(); + } + + @Override + SurfaceControl getDimLayer() { + return mDimState != null ? mDimState.mDimLayer : null; + } + + @Override + void resetDimStates() { + if (mDimState == null) { + return; + } + if (!mDimState.mDontReset) { + mDimState.mDimming = false; + } + } + + @Override + Rect getDimBounds() { + return mDimState != null ? mDimState.mDimBounds : null; + } + + @Override + void dontAnimateExit() { + if (mDimState != null) { + mDimState.mAnimateExit = false; + } + } + + @Override + protected void dim(WindowContainer container, int relativeLayer, float alpha, int blurRadius) { + final DimState d = obtainDimState(container); + + if (d == null) { + return; + } + + // The dim method is called from WindowState.prepareSurfaces(), which is always called + // in the correct Z from lowest Z to highest. This ensures that the dim layer is always + // relative to the highest Z layer with a dim. + SurfaceControl.Transaction t = mHost.getPendingTransaction(); + t.setRelativeLayer(d.mDimLayer, container.getSurfaceControl(), relativeLayer); + t.setAlpha(d.mDimLayer, alpha); + t.setBackgroundBlurRadius(d.mDimLayer, blurRadius); + + d.mDimming = true; + } + + @Override + boolean updateDims(SurfaceControl.Transaction t) { + if (mDimState == null) { + return false; + } + + if (!mDimState.mDimming) { + if (!mDimState.mAnimateExit) { + if (mDimState.mDimLayer.isValid()) { + t.remove(mDimState.mDimLayer); + } + } else { + startDimExit(mLastRequestedDimContainer, + mDimState.mSurfaceAnimator, t); + } + mDimState = null; + return false; + } else { + final Rect bounds = mDimState.mDimBounds; + // TODO: Once we use geometry from hierarchy this falls away. + t.setPosition(mDimState.mDimLayer, bounds.left, bounds.top); + t.setWindowCrop(mDimState.mDimLayer, bounds.width(), bounds.height()); + if (!mDimState.mIsVisible) { + mDimState.mIsVisible = true; + t.show(mDimState.mDimLayer); + // Skip enter animation while starting window is on top of its activity + final WindowState ws = mLastRequestedDimContainer.asWindowState(); + if (ws == null || ws.mActivityRecord == null + || ws.mActivityRecord.mStartingData == null) { + startDimEnter(mLastRequestedDimContainer, + mDimState.mSurfaceAnimator, t); + } + } + return true; + } + } + + private long getDimDuration(WindowContainer container) { + // Use the same duration as the animation on the WindowContainer + AnimationAdapter animationAdapter = container.mSurfaceAnimator.getAnimation(); + final float durationScale = container.mWmService.getTransitionAnimationScaleLocked(); + return animationAdapter == null ? (long) (DEFAULT_DIM_ANIM_DURATION * durationScale) + : animationAdapter.getDurationHint(); + } + + private void startDimEnter(WindowContainer container, SurfaceAnimator animator, + SurfaceControl.Transaction t) { + startAnim(container, animator, t, 0 /* startAlpha */, 1 /* endAlpha */); + } + + private void startDimExit(WindowContainer container, SurfaceAnimator animator, + SurfaceControl.Transaction t) { + startAnim(container, animator, t, 1 /* startAlpha */, 0 /* endAlpha */); + } + + private void startAnim(WindowContainer container, SurfaceAnimator animator, + SurfaceControl.Transaction t, float startAlpha, float endAlpha) { + mSurfaceAnimatorStarter.startAnimation(animator, t, new LocalAnimationAdapter( + new AlphaAnimationSpec(startAlpha, endAlpha, getDimDuration(container)), + mHost.mWmService.mSurfaceAnimationRunner), false /* hidden */, + ANIMATION_TYPE_DIMMER); + } + + private static class AlphaAnimationSpec implements LocalAnimationAdapter.AnimationSpec { + private final long mDuration; + private final float mFromAlpha; + private final float mToAlpha; + + AlphaAnimationSpec(float fromAlpha, float toAlpha, long duration) { + mFromAlpha = fromAlpha; + mToAlpha = toAlpha; + mDuration = duration; + } + + @Override + public long getDuration() { + return mDuration; + } + + @Override + public void apply(SurfaceControl.Transaction t, SurfaceControl sc, long currentPlayTime) { + final float fraction = getFraction(currentPlayTime); + final float alpha = fraction * (mToAlpha - mFromAlpha) + mFromAlpha; + t.setAlpha(sc, alpha); + } + + @Override + public void dump(PrintWriter pw, String prefix) { + pw.print(prefix); pw.print("from="); pw.print(mFromAlpha); + pw.print(" to="); pw.print(mToAlpha); + pw.print(" duration="); pw.println(mDuration); + } + + @Override + public void dumpDebugInner(ProtoOutputStream proto) { + final long token = proto.start(ALPHA); + proto.write(FROM, mFromAlpha); + proto.write(TO, mToAlpha); + proto.write(DURATION_MS, mDuration); + proto.end(token); + } + } +} diff --git a/services/core/java/com/android/server/wm/SmoothDimmer.java b/services/core/java/com/android/server/wm/SmoothDimmer.java new file mode 100644 index 000000000000..6ddbd2c8eb67 --- /dev/null +++ b/services/core/java/com/android/server/wm/SmoothDimmer.java @@ -0,0 +1,395 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_DIMMER; +import static com.android.server.wm.AlphaAnimationSpecProto.DURATION_MS; +import static com.android.server.wm.AlphaAnimationSpecProto.FROM; +import static com.android.server.wm.AlphaAnimationSpecProto.TO; +import static com.android.server.wm.AnimationSpecProto.ALPHA; +import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_DIMMER; +import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; + +import android.graphics.Rect; +import android.util.Log; +import android.util.proto.ProtoOutputStream; +import android.view.Surface; +import android.view.SurfaceControl; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; + +import java.io.PrintWriter; + +class SmoothDimmer extends Dimmer { + private static final String TAG = TAG_WITH_CLASS_NAME ? "Dimmer" : TAG_WM; + private static final float EPSILON = 0.0001f; + // This is in milliseconds. + private static final int DEFAULT_DIM_ANIM_DURATION = 200; + DimState mDimState; + private WindowContainer mLastRequestedDimContainer; + private final AnimationAdapterFactory mAnimationAdapterFactory; + + @VisibleForTesting + class DimState { + /** + * The layer where property changes should be invoked on. + */ + SurfaceControl mDimLayer; + boolean mDimming; + boolean mIsVisible; + + // TODO(b/64816140): Remove after confirming dimmer layer always matches its container. + final Rect mDimBounds = new Rect(); + + /** + * Determines whether the dim layer should animate before destroying. + */ + boolean mAnimateExit = true; + + /** + * Used for Dims not associated with a WindowContainer. See {@link Dimmer#dimAbove} for + * details on Dim lifecycle. + */ + boolean mDontReset; + + Change mCurrentProperties; + Change mRequestedProperties; + private AnimationSpec mAlphaAnimationSpec; + private AnimationAdapter mLocalAnimationAdapter; + + static class Change { + private float mAlpha = -1f; + private int mBlurRadius = -1; + private WindowContainer mDimmingContainer = null; + private int mRelativeLayer = -1; + private boolean mSkipAnimation = false; + + Change() {} + + Change(Change other) { + mAlpha = other.mAlpha; + mBlurRadius = other.mBlurRadius; + mDimmingContainer = other.mDimmingContainer; + mRelativeLayer = other.mRelativeLayer; + } + + @Override + public String toString() { + return "Dim state: alpha=" + mAlpha + ", blur=" + mBlurRadius + ", container=" + + mDimmingContainer + ", relativePosition=" + mRelativeLayer + + ", skipAnimation=" + mSkipAnimation; + } + } + + DimState(SurfaceControl dimLayer) { + mDimLayer = dimLayer; + mDimming = true; + mCurrentProperties = new Change(); + mRequestedProperties = new Change(); + } + + void setExitParameters(WindowContainer container) { + setRequestedParameters(container, -1, 0, 0); + } + // Sets a requested change without applying it immediately + void setRequestedParameters(WindowContainer container, int relativeLayer, float alpha, + int blurRadius) { + mRequestedProperties.mDimmingContainer = container; + mRequestedProperties.mRelativeLayer = relativeLayer; + mRequestedProperties.mAlpha = alpha; + mRequestedProperties.mBlurRadius = blurRadius; + } + + /** + * Commit the last changes we received. Called after + * {@link Change#setRequestedParameters(WindowContainer, int, float, int)} + */ + void applyChanges(SurfaceControl.Transaction t) { + if (mRequestedProperties.mDimmingContainer.mSurfaceControl == null) { + Log.w(TAG, "container " + mRequestedProperties.mDimmingContainer + + "does not have a surface"); + return; + } + if (!mDimState.mIsVisible) { + mDimState.mIsVisible = true; + t.show(mDimState.mDimLayer); + } + t.setRelativeLayer(mDimLayer, + mRequestedProperties.mDimmingContainer.getSurfaceControl(), + mRequestedProperties.mRelativeLayer); + + if (aspectChanged()) { + if (isAnimating()) { + mLocalAnimationAdapter.onAnimationCancelled(mDimLayer); + } + if (mRequestedProperties.mSkipAnimation + || (!dimmingContainerChanged() && mDimming)) { + // If the dimming container has not changed, then it is running its own + // animation, thus we can directly set the values we get requested, unless it's + // the exiting animation + ProtoLog.d(WM_DEBUG_DIMMER, + "Dim %s skipping animation and directly setting alpha=%f, blur=%d", + mDimLayer, mRequestedProperties.mAlpha, + mRequestedProperties.mBlurRadius); + t.setAlpha(mDimLayer, mRequestedProperties.mAlpha); + t.setBackgroundBlurRadius(mDimLayer, mRequestedProperties.mBlurRadius); + mRequestedProperties.mSkipAnimation = false; + } else { + startAnimation(t); + } + } + mCurrentProperties = new Change(mRequestedProperties); + } + + private void startAnimation(SurfaceControl.Transaction t) { + mAlphaAnimationSpec = getRequestedAnimationSpec(mRequestedProperties.mAlpha, + mRequestedProperties.mBlurRadius); + mLocalAnimationAdapter = mAnimationAdapterFactory.get(mAlphaAnimationSpec, + mHost.mWmService.mSurfaceAnimationRunner); + + mLocalAnimationAdapter.startAnimation(mDimLayer, t, + ANIMATION_TYPE_DIMMER, (type, animator) -> { + t.setAlpha(mDimLayer, mRequestedProperties.mAlpha); + t.setBackgroundBlurRadius(mDimLayer, mRequestedProperties.mBlurRadius); + if (mRequestedProperties.mAlpha == 0f && !mDimming) { + ProtoLog.d(WM_DEBUG_DIMMER, + "Removing dim surface %s on transaction %s", mDimLayer, t); + t.remove(mDimLayer); + } + mLocalAnimationAdapter = null; + mAlphaAnimationSpec = null; + }); + } + + private boolean isAnimating() { + return mAlphaAnimationSpec != null; + } + + private boolean aspectChanged() { + return Math.abs(mRequestedProperties.mAlpha - mCurrentProperties.mAlpha) > EPSILON + || mRequestedProperties.mBlurRadius != mCurrentProperties.mBlurRadius; + } + + private boolean dimmingContainerChanged() { + return mRequestedProperties.mDimmingContainer != mCurrentProperties.mDimmingContainer; + } + + private AnimationSpec getRequestedAnimationSpec(float targetAlpha, int targetBlur) { + final float startAlpha; + final int startBlur; + if (mAlphaAnimationSpec != null) { + startAlpha = mAlphaAnimationSpec.mCurrentAlpha; + startBlur = mAlphaAnimationSpec.mCurrentBlur; + } else { + startAlpha = Math.max(mCurrentProperties.mAlpha, 0f); + startBlur = Math.max(mCurrentProperties.mBlurRadius, 0); + } + long duration = (long) (getDimDuration(mRequestedProperties.mDimmingContainer) + * Math.abs(targetAlpha - startAlpha)); + + ProtoLog.v(WM_DEBUG_DIMMER, "Starting animation on dim layer %s, requested by %s, " + + "alpha: %f -> %f, blur: %d -> %d", + mDimLayer, mRequestedProperties.mDimmingContainer, startAlpha, targetAlpha, + startBlur, targetBlur); + return new AnimationSpec( + new AnimationExtremes<>(startAlpha, targetAlpha), + new AnimationExtremes<>(startBlur, targetBlur), + duration + ); + } + } + + protected SmoothDimmer(WindowContainer host) { + this(host, new AnimationAdapterFactory()); + } + + @VisibleForTesting + SmoothDimmer(WindowContainer host, AnimationAdapterFactory animationFactory) { + super(host); + mAnimationAdapterFactory = animationFactory; + } + + private DimState obtainDimState(WindowContainer container) { + if (mDimState == null) { + try { + final SurfaceControl ctl = makeDimLayer(); + mDimState = new DimState(ctl); + } catch (Surface.OutOfResourcesException e) { + Log.w(TAG, "OutOfResourcesException creating dim surface"); + } + } + + mLastRequestedDimContainer = container; + return mDimState; + } + + private SurfaceControl makeDimLayer() { + return mHost.makeChildSurface(null) + .setParent(mHost.getSurfaceControl()) + .setColorLayer() + .setName("Dim Layer for - " + mHost.getName()) + .setCallsite("Dimmer.makeDimLayer") + .build(); + } + + @Override + SurfaceControl getDimLayer() { + return mDimState != null ? mDimState.mDimLayer : null; + } + + @Override + void resetDimStates() { + if (mDimState == null) { + return; + } + if (!mDimState.mDontReset) { + mDimState.mDimming = false; + } + } + + @Override + Rect getDimBounds() { + return mDimState != null ? mDimState.mDimBounds : null; + } + + @Override + void dontAnimateExit() { + if (mDimState != null) { + mDimState.mAnimateExit = false; + } + } + + @Override + protected void dim(WindowContainer container, int relativeLayer, float alpha, int blurRadius) { + final DimState d = obtainDimState(container); + + mDimState.mRequestedProperties.mDimmingContainer = container; + mDimState.setRequestedParameters(container, relativeLayer, alpha, blurRadius); + d.mDimming = true; + } + + boolean updateDims(SurfaceControl.Transaction t) { + if (mDimState == null) { + return false; + } + + if (!mDimState.mDimming) { + // No one is dimming anymore, fade out dim and remove + if (!mDimState.mAnimateExit) { + if (mDimState.mDimLayer.isValid()) { + t.remove(mDimState.mDimLayer); + } + } else { + mDimState.setExitParameters( + mDimState.mRequestedProperties.mDimmingContainer); + mDimState.applyChanges(t); + } + mDimState = null; + return false; + } + final Rect bounds = mDimState.mDimBounds; + // TODO: Once we use geometry from hierarchy this falls away. + t.setPosition(mDimState.mDimLayer, bounds.left, bounds.top); + t.setWindowCrop(mDimState.mDimLayer, bounds.width(), bounds.height()); + // Skip enter animation while starting window is on top of its activity + final WindowState ws = mLastRequestedDimContainer.asWindowState(); + if (!mDimState.mIsVisible && ws != null && ws.mActivityRecord != null + && ws.mActivityRecord.mStartingData != null) { + mDimState.mRequestedProperties.mSkipAnimation = true; + } + mDimState.applyChanges(t); + return true; + } + + private long getDimDuration(WindowContainer container) { + // Use the same duration as the animation on the WindowContainer + AnimationAdapter animationAdapter = container.mSurfaceAnimator.getAnimation(); + final float durationScale = container.mWmService.getTransitionAnimationScaleLocked(); + return animationAdapter == null ? (long) (DEFAULT_DIM_ANIM_DURATION * durationScale) + : animationAdapter.getDurationHint(); + } + + private static class AnimationExtremes { + final T mStartValue; + final T mFinishValue; + + AnimationExtremes(T fromValue, T toValue) { + mStartValue = fromValue; + mFinishValue = toValue; + } + } + + private static class AnimationSpec implements LocalAnimationAdapter.AnimationSpec { + private final long mDuration; + private final AnimationExtremes mAlpha; + private final AnimationExtremes mBlur; + + float mCurrentAlpha = 0; + int mCurrentBlur = 0; + + AnimationSpec(AnimationExtremes alpha, + AnimationExtremes blur, long duration) { + mAlpha = alpha; + mBlur = blur; + mDuration = duration; + } + + @Override + public long getDuration() { + return mDuration; + } + + @Override + public void apply(SurfaceControl.Transaction t, SurfaceControl sc, long currentPlayTime) { + final float fraction = getFraction(currentPlayTime); + mCurrentAlpha = + fraction * (mAlpha.mFinishValue - mAlpha.mStartValue) + mAlpha.mStartValue; + mCurrentBlur = + (int) fraction * (mBlur.mFinishValue - mBlur.mStartValue) + mBlur.mStartValue; + t.setAlpha(sc, mCurrentAlpha); + t.setBackgroundBlurRadius(sc, mCurrentBlur); + } + + @Override + public void dump(PrintWriter pw, String prefix) { + pw.print(prefix); pw.print("from_alpha="); pw.print(mAlpha.mStartValue); + pw.print(" to_alpha="); pw.print(mAlpha.mFinishValue); + pw.print(prefix); pw.print("from_blur="); pw.print(mBlur.mStartValue); + pw.print(" to_blur="); pw.print(mBlur.mFinishValue); + pw.print(" duration="); pw.println(mDuration); + } + + @Override + public void dumpDebugInner(ProtoOutputStream proto) { + final long token = proto.start(ALPHA); + proto.write(FROM, mAlpha.mStartValue); + proto.write(TO, mAlpha.mFinishValue); + proto.write(DURATION_MS, mDuration); + proto.end(token); + } + } + + static class AnimationAdapterFactory { + + public AnimationAdapter get(LocalAnimationAdapter.AnimationSpec alphaAnimationSpec, + SurfaceAnimationRunner runner) { + return new LocalAnimationAdapter(alphaAnimationSpec, runner); + } + } +} diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java index 408ea6eb8c1a..c3de4d5acc21 100644 --- a/services/core/java/com/android/server/wm/SurfaceAnimator.java +++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java @@ -49,7 +49,8 @@ import java.util.function.Supplier; * {@link AnimationAdapter}. When the animation is done animating, our callback to finish the * animation will be invoked, at which we reparent the children back to the original parent. */ -class SurfaceAnimator { +@VisibleForTesting +public class SurfaceAnimator { private static final String TAG = TAG_WITH_CLASS_NAME ? "SurfaceAnimator" : TAG_WM; @@ -617,7 +618,8 @@ class SurfaceAnimator { * Callback to be passed into {@link AnimationAdapter#startAnimation} to be invoked by the * component that is running the animation when the animation is finished. */ - interface OnAnimationFinishedCallback { + @VisibleForTesting + public interface OnAnimationFinishedCallback { void onAnimationFinished(@AnimationType int type, AnimationAdapter anim); } diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 50bc825d8bea..4a3314d012a0 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -103,6 +103,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.server.am.HostingRecord; import com.android.server.pm.pkg.AndroidPackage; +import com.android.window.flags.Flags; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -209,7 +210,8 @@ class TaskFragment extends WindowContainer { */ int mMinHeight; - Dimmer mDimmer = new Dimmer(this); + Dimmer mDimmer = Flags.dimmerRefactor() + ? new SmoothDimmer(this) : new LegacyDimmer(this); /** This task fragment will be removed when the cleanup of its children are done. */ private boolean mIsRemovalRequested; diff --git a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java index 233a2076a867..84d42d42f834 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java @@ -18,16 +18,20 @@ package com.android.server.wm; 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.reset; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_DIMMER; +import static com.android.server.wm.utils.LastCallVerifier.lastCall; import static org.junit.Assert.assertNotNull; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; 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.reset; import static org.mockito.Mockito.when; import android.graphics.Rect; @@ -35,8 +39,9 @@ import android.platform.test.annotations.Presubmit; import android.view.SurfaceControl; import android.view.SurfaceSession; -import com.android.server.wm.SurfaceAnimator.AnimationType; import com.android.server.testutils.StubTransaction; +import com.android.server.wm.utils.MockAnimationAdapter; +import com.android.window.flags.Flags; import org.junit.Before; import org.junit.Test; @@ -118,102 +123,168 @@ public class DimmerTests extends WindowTestsBase { } } - private MockSurfaceBuildingContainer mHost; - private Dimmer mDimmer; - private SurfaceControl.Transaction mTransaction; - private Dimmer.SurfaceAnimatorStarter mSurfaceAnimatorStarter; + static class MockAnimationAdapterFactory extends SmoothDimmer.AnimationAdapterFactory { + public AnimationAdapter get(LocalAnimationAdapter.AnimationSpec alphaAnimationSpec, + SurfaceAnimationRunner runner) { + return sTestAnimation; + } + } - private static class SurfaceAnimatorStarterImpl implements Dimmer.SurfaceAnimatorStarter { + private static class SurfaceAnimatorStarterImpl implements LegacyDimmer.SurfaceAnimatorStarter { @Override public void startAnimation(SurfaceAnimator surfaceAnimator, SurfaceControl.Transaction t, - AnimationAdapter anim, boolean hidden, @AnimationType int type) { + AnimationAdapter anim, boolean hidden, @SurfaceAnimator.AnimationType int type) { surfaceAnimator.mStaticAnimationFinishedCallback.onAnimationFinished(type, anim); } } + private MockSurfaceBuildingContainer mHost; + private Dimmer mDimmer; + private SurfaceControl.Transaction mTransaction; + private TestWindowContainer mChild; + private static AnimationAdapter sTestAnimation; + private static LegacyDimmer.SurfaceAnimatorStarter sSurfaceAnimatorStarter; + @Before public void setUp() throws Exception { mHost = new MockSurfaceBuildingContainer(mWm); - mSurfaceAnimatorStarter = spy(new SurfaceAnimatorStarterImpl()); mTransaction = spy(StubTransaction.class); - mDimmer = new Dimmer(mHost, mSurfaceAnimatorStarter); + mChild = new TestWindowContainer(mWm); + if (Flags.dimmerRefactor()) { + sTestAnimation = spy(new MockAnimationAdapter()); + mDimmer = new SmoothDimmer(mHost, new MockAnimationAdapterFactory()); + } else { + sSurfaceAnimatorStarter = spy(new SurfaceAnimatorStarterImpl()); + mDimmer = new LegacyDimmer(mHost, sSurfaceAnimatorStarter); + } } @Test public void testUpdateDimsAppliesCrop() { - TestWindowContainer child = new TestWindowContainer(mWm); - mHost.addChild(child, 0); + mHost.addChild(mChild, 0); final float alpha = 0.8f; - mDimmer.dimAbove(child, alpha); + mDimmer.dimAbove(mChild, alpha); int width = 100; int height = 300; - mDimmer.mDimState.mDimBounds.set(0, 0, width, height); + mDimmer.getDimBounds().set(0, 0, width, height); mDimmer.updateDims(mTransaction); - verify(mTransaction).setWindowCrop(getDimLayer(), width, height); - verify(mTransaction).show(getDimLayer()); + verify(mTransaction).setWindowCrop(mDimmer.getDimLayer(), width, height); + verify(mTransaction).show(mDimmer.getDimLayer()); } @Test - public void testDimAboveWithChildCreatesSurfaceAboveChild() { - TestWindowContainer child = new TestWindowContainer(mWm); - mHost.addChild(child, 0); + public void testDimAboveWithChildCreatesSurfaceAboveChild_Smooth() { + assumeTrue(Flags.dimmerRefactor()); + final float alpha = 0.8f; + mHost.addChild(mChild, 0); + mDimmer.dimAbove(mChild, alpha); + SurfaceControl dimLayer = mDimmer.getDimLayer(); + + assertNotNull("Dimmer should have created a surface", dimLayer); + + mDimmer.updateDims(mTransaction); + verify(sTestAnimation).startAnimation(eq(dimLayer), eq(mTransaction), + anyInt(), any(SurfaceAnimator.OnAnimationFinishedCallback.class)); + verify(mTransaction).setRelativeLayer(dimLayer, mChild.mControl, 1); + verify(mTransaction, lastCall()).setAlpha(dimLayer, alpha); + } + @Test + public void testDimAboveWithChildCreatesSurfaceAboveChild_Legacy() { + assumeFalse(Flags.dimmerRefactor()); final float alpha = 0.8f; - mDimmer.dimAbove(child, alpha); - SurfaceControl dimLayer = getDimLayer(); + mHost.addChild(mChild, 0); + mDimmer.dimAbove(mChild, alpha); + SurfaceControl dimLayer = mDimmer.getDimLayer(); assertNotNull("Dimmer should have created a surface", dimLayer); verify(mHost.getPendingTransaction()).setAlpha(dimLayer, alpha); - verify(mHost.getPendingTransaction()).setRelativeLayer(dimLayer, child.mControl, 1); + verify(mHost.getPendingTransaction()).setRelativeLayer(dimLayer, mChild.mControl, 1); } @Test - public void testDimBelowWithChildSurfaceCreatesSurfaceBelowChild() { - TestWindowContainer child = new TestWindowContainer(mWm); - mHost.addChild(child, 0); + public void testDimBelowWithChildSurfaceCreatesSurfaceBelowChild_Smooth() { + assumeTrue(Flags.dimmerRefactor()); + final float alpha = 0.7f; + mHost.addChild(mChild, 0); + mDimmer.dimBelow(mChild, alpha, 50); + SurfaceControl dimLayer = mDimmer.getDimLayer(); - final float alpha = 0.8f; - mDimmer.dimBelow(child, alpha, 0); - SurfaceControl dimLayer = getDimLayer(); + assertNotNull("Dimmer should have created a surface", dimLayer); + + mDimmer.updateDims(mTransaction); + verify(sTestAnimation).startAnimation(eq(dimLayer), eq(mTransaction), + anyInt(), any(SurfaceAnimator.OnAnimationFinishedCallback.class)); + verify(mTransaction).setRelativeLayer(dimLayer, mChild.mControl, -1); + verify(mTransaction, lastCall()).setAlpha(dimLayer, alpha); + verify(mTransaction).setBackgroundBlurRadius(dimLayer, 50); + } + + @Test + public void testDimBelowWithChildSurfaceCreatesSurfaceBelowChild_Legacy() { + assumeFalse(Flags.dimmerRefactor()); + final float alpha = 0.7f; + mHost.addChild(mChild, 0); + mDimmer.dimBelow(mChild, alpha, 50); + SurfaceControl dimLayer = mDimmer.getDimLayer(); assertNotNull("Dimmer should have created a surface", dimLayer); verify(mHost.getPendingTransaction()).setAlpha(dimLayer, alpha); - verify(mHost.getPendingTransaction()).setRelativeLayer(dimLayer, child.mControl, -1); + verify(mHost.getPendingTransaction()).setRelativeLayer(dimLayer, mChild.mControl, -1); } @Test - public void testDimBelowWithChildSurfaceDestroyedWhenReset() { - TestWindowContainer child = new TestWindowContainer(mWm); - mHost.addChild(child, 0); + public void testDimBelowWithChildSurfaceDestroyedWhenReset_Smooth() { + assumeTrue(Flags.dimmerRefactor()); + mHost.addChild(mChild, 0); + + final float alpha = 0.8f; + // Dim once + mDimmer.dimBelow(mChild, alpha, 0); + SurfaceControl dimLayer = mDimmer.getDimLayer(); + mDimmer.updateDims(mTransaction); + // Reset, and don't dim + mDimmer.resetDimStates(); + mDimmer.updateDims(mTransaction); + verify(mTransaction).show(dimLayer); + verify(mTransaction).remove(dimLayer); + } + + @Test + public void testDimBelowWithChildSurfaceDestroyedWhenReset_Legacy() { + assumeFalse(Flags.dimmerRefactor()); + mHost.addChild(mChild, 0); final float alpha = 0.8f; - mDimmer.dimAbove(child, alpha); - SurfaceControl dimLayer = getDimLayer(); + mDimmer.dimAbove(mChild, alpha); + SurfaceControl dimLayer = mDimmer.getDimLayer(); mDimmer.resetDimStates(); mDimmer.updateDims(mTransaction); - verify(mSurfaceAnimatorStarter).startAnimation(any(SurfaceAnimator.class), any( - SurfaceControl.Transaction.class), any(AnimationAdapter.class), anyBoolean(), + verify(sSurfaceAnimatorStarter).startAnimation(any(SurfaceAnimator.class), + any(SurfaceControl.Transaction.class), any(AnimationAdapter.class), + anyBoolean(), eq(ANIMATION_TYPE_DIMMER)); verify(mHost.getPendingTransaction()).remove(dimLayer); } @Test public void testDimBelowWithChildSurfaceNotDestroyedWhenPersisted() { - TestWindowContainer child = new TestWindowContainer(mWm); - mHost.addChild(child, 0); + mHost.addChild(mChild, 0); final float alpha = 0.8f; - mDimmer.dimAbove(child, alpha); - SurfaceControl dimLayer = getDimLayer(); + // Dim once + mDimmer.dimBelow(mChild, alpha, 0); + SurfaceControl dimLayer = mDimmer.getDimLayer(); + mDimmer.updateDims(mTransaction); + // Reset and dim again mDimmer.resetDimStates(); - mDimmer.dimAbove(child, alpha); - + mDimmer.dimAbove(mChild, alpha); mDimmer.updateDims(mTransaction); verify(mTransaction).show(dimLayer); verify(mTransaction, never()).remove(dimLayer); @@ -221,14 +292,12 @@ public class DimmerTests extends WindowTestsBase { @Test public void testDimUpdateWhileDimming() { - TestWindowContainer child = new TestWindowContainer(mWm); - mHost.addChild(child, 0); - + mHost.addChild(mChild, 0); final float alpha = 0.8f; - mDimmer.dimAbove(child, alpha); - final Rect bounds = mDimmer.mDimState.mDimBounds; + mDimmer.dimAbove(mChild, alpha); + final Rect bounds = mDimmer.getDimBounds(); - SurfaceControl dimLayer = getDimLayer(); + SurfaceControl dimLayer = mDimmer.getDimLayer(); bounds.set(0, 0, 10, 10); mDimmer.updateDims(mTransaction); verify(mTransaction).setWindowCrop(dimLayer, bounds.width(), bounds.height()); @@ -242,41 +311,56 @@ public class DimmerTests extends WindowTestsBase { } @Test - public void testRemoveDimImmediately() { - TestWindowContainer child = new TestWindowContainer(mWm); - mHost.addChild(child, 0); + public void testRemoveDimImmediately_Smooth() { + assumeTrue(Flags.dimmerRefactor()); + mHost.addChild(mChild, 0); + mDimmer.dimAbove(mChild, 1); + SurfaceControl dimLayer = mDimmer.getDimLayer(); + mDimmer.updateDims(mTransaction); + verify(mTransaction, times(1)).show(dimLayer); - mDimmer.dimAbove(child, 1); - SurfaceControl dimLayer = getDimLayer(); + reset(sTestAnimation); + mDimmer.dontAnimateExit(); + mDimmer.resetDimStates(); + mDimmer.updateDims(mTransaction); + verify(sTestAnimation, never()).startAnimation( + any(SurfaceControl.class), any(SurfaceControl.Transaction.class), + anyInt(), any(SurfaceAnimator.OnAnimationFinishedCallback.class)); + verify(mTransaction).remove(dimLayer); + } + + @Test + public void testRemoveDimImmediately_Legacy() { + assumeFalse(Flags.dimmerRefactor()); + mHost.addChild(mChild, 0); + mDimmer.dimAbove(mChild, 1); + SurfaceControl dimLayer = mDimmer.getDimLayer(); mDimmer.updateDims(mTransaction); verify(mTransaction, times(1)).show(dimLayer); - reset(mSurfaceAnimatorStarter); + reset(sSurfaceAnimatorStarter); mDimmer.dontAnimateExit(); mDimmer.resetDimStates(); mDimmer.updateDims(mTransaction); - verify(mSurfaceAnimatorStarter, never()).startAnimation(any(SurfaceAnimator.class), any( - SurfaceControl.Transaction.class), any(AnimationAdapter.class), anyBoolean(), + verify(sSurfaceAnimatorStarter, never()).startAnimation(any(SurfaceAnimator.class), + any(SurfaceControl.Transaction.class), any(AnimationAdapter.class), anyBoolean(), eq(ANIMATION_TYPE_DIMMER)); verify(mTransaction).remove(dimLayer); } @Test - public void testDimmerWithBlurUpdatesTransaction() { + public void testDimmerWithBlurUpdatesTransaction_Legacy() { + assumeFalse(Flags.dimmerRefactor()); TestWindowContainer child = new TestWindowContainer(mWm); mHost.addChild(child, 0); final int blurRadius = 50; mDimmer.dimBelow(child, 0, blurRadius); - SurfaceControl dimLayer = getDimLayer(); + SurfaceControl dimLayer = mDimmer.getDimLayer(); assertNotNull("Dimmer should have created a surface", dimLayer); verify(mHost.getPendingTransaction()).setBackgroundBlurRadius(dimLayer, blurRadius); verify(mHost.getPendingTransaction()).setRelativeLayer(dimLayer, child.mControl, -1); } - - private SurfaceControl getDimLayer() { - return mDimmer.mDimState.mDimLayer; - } } diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/LastCallVerifier.java b/services/tests/wmtests/src/com/android/server/wm/utils/LastCallVerifier.java new file mode 100644 index 000000000000..320d09444681 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/utils/LastCallVerifier.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.utils; + +import org.mockito.internal.verification.VerificationModeFactory; +import org.mockito.internal.verification.api.VerificationData; +import org.mockito.invocation.Invocation; +import org.mockito.invocation.MatchableInvocation; +import org.mockito.verification.VerificationMode; + +import java.util.List; + +/** + * Verifier to check that the last call of a method received the expected argument + */ +public class LastCallVerifier implements VerificationMode { + + /** + * Allows comparing the expected invocation with the last invocation on the same method + */ + public static LastCallVerifier lastCall() { + return new LastCallVerifier(); + } + + @Override + public void verify(VerificationData data) { + List invocations = data.getAllInvocations(); + MatchableInvocation target = data.getTarget(); + for (int i = invocations.size() - 1; i >= 0; i--) { + final Invocation invocation = invocations.get(i); + if (target.hasSameMethod(invocation)) { + if (target.matches(invocation)) { + return; + } else { + throw new LastCallMismatch(target.getInvocation(), invocation, invocations); + } + } + } + throw new RuntimeException(target + " never invoked"); + } + + @Override + public VerificationMode description(String description) { + return VerificationModeFactory.description(this, description); + } + + static class LastCallMismatch extends RuntimeException { + LastCallMismatch( + Invocation expected, Invocation received, List allInvocations) { + super("Expected invocation " + expected + " but received " + received + + " as the last invocation.\nAll registered invocations:\n" + allInvocations); + } + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/MockAnimationAdapter.java b/services/tests/wmtests/src/com/android/server/wm/utils/MockAnimationAdapter.java new file mode 100644 index 000000000000..1a66970a8dc2 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/utils/MockAnimationAdapter.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.utils; + +import android.util.proto.ProtoOutputStream; +import android.view.SurfaceControl; + +import androidx.annotation.NonNull; + +import com.android.server.wm.AnimationAdapter; +import com.android.server.wm.SurfaceAnimator; + +import java.io.PrintWriter; + +/** + * An empty animation adapter which just executes the finish callback + */ +public class MockAnimationAdapter implements AnimationAdapter { + + @Override + public boolean getShowWallpaper() { + return false; + } + + @Override + public void startAnimation(SurfaceControl animationLeash, SurfaceControl.Transaction t, + int type, @NonNull SurfaceAnimator.OnAnimationFinishedCallback finishCallback) { + // As the animation won't run, finish it immediately + finishCallback.onAnimationFinished(0, null); + } + + @Override + public void onAnimationCancelled(SurfaceControl animationLeash) {} + + @Override + public long getDurationHint() { + return 0; + } + + @Override + public long getStatusBarTransitionsStartTime() { + return 0; + } + + @Override + public void dump(PrintWriter pw, String prefix) {} + + @Override + public void dumpDebug(ProtoOutputStream proto) {} +} -- cgit v1.2.3-59-g8ed1b From 910509a5ba18e4a0b5681116ff5faf881d26068a Mon Sep 17 00:00:00 2001 From: Pierre Barbier de Reuille Date: Mon, 16 Oct 2023 20:46:57 +0100 Subject: Adapt recorded scaling in X and Y The new scaling takes into account the asymmetry in pixel size on the external display for better rendering. Bug: 304248677 Test: atest WmTests:ContentRecorderTests Change-Id: I7502538c5423343da93c20bfb7c6c8bea33dd7d2 --- data/etc/services.core.protolog.json | 12 +- .../com/android/server/wm/ContentRecorder.java | 79 ++++++-- .../android/server/wm/ContentRecorderTests.java | 217 ++++++++++++++++++--- 3 files changed, 265 insertions(+), 43 deletions(-) (limited to 'data') diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index b71aaf3fc2e6..cc73ecee2146 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -2887,6 +2887,12 @@ "group": "WM_DEBUG_RESIZE", "at": "com\/android\/server\/wm\/WindowState.java" }, + "419378610": { + "message": "Content Recording: Apply transformations of shift %d x %d, scale %f x %f, crop (aka recorded content size) %d x %d for display %d; display has size %d x %d; surface has size %d x %d", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecorder.java" + }, "422634333": { "message": "First draw done in potential wallpaper target %s", "level": "VERBOSE", @@ -4339,12 +4345,6 @@ "group": "WM_DEBUG_REMOTE_ANIMATIONS", "at": "com\/android\/server\/wm\/RemoteAnimationController.java" }, - "1936800105": { - "message": "Content Recording: Apply transformations of shift %d x %d, scale %f, crop (aka recorded content size) %d x %d for display %d; display has size %d x %d; surface has size %d x %d", - "level": "VERBOSE", - "group": "WM_DEBUG_CONTENT_RECORDING", - "at": "com\/android\/server\/wm\/ContentRecorder.java" - }, "1945495497": { "message": "Focused window didn't have a valid surface drawn.", "level": "DEBUG", diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java index 06448d0c1e84..022ef6152929 100644 --- a/services/core/java/com/android/server/wm/ContentRecorder.java +++ b/services/core/java/com/android/server/wm/ContentRecorder.java @@ -28,6 +28,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.res.Configuration; import android.graphics.Point; +import android.graphics.PointF; import android.graphics.Rect; import android.media.projection.IMediaProjectionManager; import android.os.IBinder; @@ -36,16 +37,28 @@ import android.os.ServiceManager; import android.view.ContentRecordingSession; import android.view.ContentRecordingSession.RecordContent; import android.view.Display; +import android.view.DisplayInfo; import android.view.SurfaceControl; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; +import com.android.server.display.feature.DisplayManagerFlags; /** * Manages content recording for a particular {@link DisplayContent}. */ final class ContentRecorder implements WindowContainerListener { + /** + * Maximum acceptable anisotropy for the output image. + * + * Necessary to avoid unnecessary scaling when the anisotropy is almost the same, as it is not + * exact anyway. For external displays, we expect an anisoptry of about 2% even if the pixels + * are, in fact, square due to the imprecision of the display's actual size (rounded to the + * nearest cm). + */ + private static final float MAX_ANISOTROPY = 0.025f; + /** * The display content this class is handling recording for. */ @@ -87,15 +100,20 @@ final class ContentRecorder implements WindowContainerListener { @Configuration.Orientation private int mLastOrientation = ORIENTATION_UNDEFINED; + private final boolean mCorrectForAnisotropicPixels; + ContentRecorder(@NonNull DisplayContent displayContent) { - this(displayContent, new RemoteMediaProjectionManagerWrapper(displayContent.mDisplayId)); + this(displayContent, new RemoteMediaProjectionManagerWrapper(displayContent.mDisplayId), + new DisplayManagerFlags().isConnectedDisplayManagementEnabled()); } @VisibleForTesting ContentRecorder(@NonNull DisplayContent displayContent, - @NonNull MediaProjectionManagerWrapper mediaProjectionManager) { + @NonNull MediaProjectionManagerWrapper mediaProjectionManager, + boolean correctForAnisotropicPixels) { mDisplayContent = displayContent; mMediaProjectionManager = mediaProjectionManager; + mCorrectForAnisotropicPixels = correctForAnisotropicPixels; } /** @@ -460,6 +478,33 @@ final class ContentRecorder implements WindowContainerListener { } } + private void computeScaling(int inputSizeX, int inputSizeY, + float inputDpiX, float inputDpiY, + int outputSizeX, int outputSizeY, + float outputDpiX, float outputDpiY, + PointF scaleOut) { + float relAnisotropy = (inputDpiY / inputDpiX) / (outputDpiY / outputDpiX); + if (!mCorrectForAnisotropicPixels + || (relAnisotropy > (1 - MAX_ANISOTROPY) && relAnisotropy < (1 + MAX_ANISOTROPY))) { + // Calculate the scale to apply to the root mirror SurfaceControl to fit the size of the + // output surface. + float scaleX = outputSizeX / (float) inputSizeX; + float scaleY = outputSizeY / (float) inputSizeY; + float scale = Math.min(scaleX, scaleY); + scaleOut.x = scale; + scaleOut.y = scale; + return; + } + + float relDpiX = outputDpiX / inputDpiX; + float relDpiY = outputDpiY / inputDpiY; + + float scale = Math.min(outputSizeX / relDpiX / inputSizeX, + outputSizeY / relDpiY / inputSizeY); + scaleOut.x = scale * relDpiX; + scaleOut.y = scale * relDpiY; + } + /** * Apply transformations to the mirrored surface to ensure the captured contents are scaled to * fit and centred in the output surface. @@ -473,13 +518,19 @@ final class ContentRecorder implements WindowContainerListener { */ @VisibleForTesting void updateMirroredSurface(SurfaceControl.Transaction transaction, Rect recordedContentBounds, Point surfaceSize) { - // Calculate the scale to apply to the root mirror SurfaceControl to fit the size of the - // output surface. - float scaleX = surfaceSize.x / (float) recordedContentBounds.width(); - float scaleY = surfaceSize.y / (float) recordedContentBounds.height(); - float scale = Math.min(scaleX, scaleY); - int scaledWidth = Math.round(scale * (float) recordedContentBounds.width()); - int scaledHeight = Math.round(scale * (float) recordedContentBounds.height()); + + DisplayInfo inputDisplayInfo = mRecordedWindowContainer.mDisplayContent.getDisplayInfo(); + DisplayInfo outputDisplayInfo = mDisplayContent.getDisplayInfo(); + + PointF scale = new PointF(); + computeScaling(recordedContentBounds.width(), recordedContentBounds.height(), + inputDisplayInfo.physicalXDpi, inputDisplayInfo.physicalYDpi, + surfaceSize.x, surfaceSize.y, + outputDisplayInfo.physicalXDpi, outputDisplayInfo.physicalYDpi, + scale); + + int scaledWidth = Math.round(scale.x * (float) recordedContentBounds.width()); + int scaledHeight = Math.round(scale.y * (float) recordedContentBounds.height()); // Calculate the shift to apply to the root mirror SurfaceControl to centre the mirrored // contents in the output surface. @@ -493,10 +544,10 @@ final class ContentRecorder implements WindowContainerListener { } ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, - "Content Recording: Apply transformations of shift %d x %d, scale %f, crop (aka " - + "recorded content size) %d x %d for display %d; display has size %d x " - + "%d; surface has size %d x %d", - shiftedX, shiftedY, scale, recordedContentBounds.width(), + "Content Recording: Apply transformations of shift %d x %d, scale %f x %f, crop " + + "(aka recorded content size) %d x %d for display %d; display has size " + + "%d x %d; surface has size %d x %d", + shiftedX, shiftedY, scale.x, scale.y, recordedContentBounds.width(), recordedContentBounds.height(), mDisplayContent.getDisplayId(), mDisplayContent.getConfiguration().screenWidthDp, mDisplayContent.getConfiguration().screenHeightDp, surfaceSize.x, surfaceSize.y); @@ -508,7 +559,7 @@ final class ContentRecorder implements WindowContainerListener { recordedContentBounds.height()) // Scale the root mirror SurfaceControl, based upon the size difference between the // source (DisplayArea to capture) and output (surface the app reads images from). - .setMatrix(mRecordedSurface, scale, 0 /* dtdx */, 0 /* dtdy */, scale) + .setMatrix(mRecordedSurface, scale.x, 0 /* dtdx */, 0 /* dtdy */, scale.y) // Position needs to be updated when the mirrored DisplayArea has changed, since // the content will no longer be centered in the output surface. .setPosition(mRecordedSurface, shiftedX /* x */, shiftedY /* y */); diff --git a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java index d2eb1cc0222b..78566fb06179 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java @@ -84,31 +84,49 @@ public class ContentRecorderTests extends WindowTestsBase { private final ContentRecordingSession mWaitingDisplaySession = ContentRecordingSession.createDisplaySession(DEFAULT_DISPLAY); private ContentRecordingSession mTaskSession; - private static Point sSurfaceSize; + private Point mSurfaceSize; private ContentRecorder mContentRecorder; @Mock private MediaProjectionManagerWrapper mMediaProjectionManagerWrapper; private SurfaceControl mRecordedSurface; + private boolean mHandleAnisotropicDisplayMirroring = false; + @Before public void setUp() { MockitoAnnotations.initMocks(this); - // GIVEN SurfaceControl can successfully mirror the provided surface. - sSurfaceSize = new Point( - mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().width(), + doReturn(INVALID_DISPLAY).when(mWm.mDisplayManagerInternal).getDisplayIdToMirror(anyInt()); + + // Skip unnecessary operations of relayout. + spyOn(mWm.mWindowPlacerLocked); + doNothing().when(mWm.mWindowPlacerLocked).performSurfacePlacement(anyBoolean()); + } + + private void defaultInit() { + createContentRecorder(createDefaultDisplayInfo()); + } + + private DisplayInfo createDefaultDisplayInfo() { + return createDisplayInfo(mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().width(), mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().height()); - mRecordedSurface = surfaceControlMirrors(sSurfaceSize); + } - doReturn(INVALID_DISPLAY).when(mWm.mDisplayManagerInternal).getDisplayIdToMirror(anyInt()); + private DisplayInfo createDisplayInfo(int width, int height) { + // GIVEN SurfaceControl can successfully mirror the provided surface. + mSurfaceSize = new Point(width, height); + mRecordedSurface = surfaceControlMirrors(mSurfaceSize); - // GIVEN the VirtualDisplay associated with the session (so the display has state ON). DisplayInfo displayInfo = mDisplayInfo; - displayInfo.logicalWidth = sSurfaceSize.x; - displayInfo.logicalHeight = sSurfaceSize.y; + displayInfo.logicalWidth = width; + displayInfo.logicalHeight = height; displayInfo.state = STATE_ON; + return displayInfo; + } + + private void createContentRecorder(DisplayInfo displayInfo) { mVirtualDisplayContent = createNewDisplay(displayInfo); final int displayId = mVirtualDisplayContent.getDisplayId(); mContentRecorder = new ContentRecorder(mVirtualDisplayContent, - mMediaProjectionManagerWrapper); + mMediaProjectionManagerWrapper, mHandleAnisotropicDisplayMirroring); spyOn(mVirtualDisplayContent); // GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to @@ -124,14 +142,11 @@ public class ContentRecorderTests extends WindowTestsBase { // GIVEN a session is waiting for the user to review consent. mWaitingDisplaySession.setVirtualDisplayId(displayId); mWaitingDisplaySession.setWaitingForConsent(true); - - // Skip unnecessary operations of relayout. - spyOn(mWm.mWindowPlacerLocked); - doNothing().when(mWm.mWindowPlacerLocked).performSurfacePlacement(anyBoolean()); } @Test public void testIsCurrentlyRecording() { + defaultInit(); assertThat(mContentRecorder.isCurrentlyRecording()).isFalse(); mContentRecorder.updateRecording(); @@ -140,6 +155,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testUpdateRecording_display() { + defaultInit(); mContentRecorder.setContentRecordingSession(mDisplaySession); mContentRecorder.updateRecording(); assertThat(mContentRecorder.isCurrentlyRecording()).isTrue(); @@ -147,6 +163,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testUpdateRecording_display_invalidDisplayIdToMirror() { + defaultInit(); ContentRecordingSession session = ContentRecordingSession.createDisplaySession( INVALID_DISPLAY); mContentRecorder.setContentRecordingSession(session); @@ -156,6 +173,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testUpdateRecording_display_noDisplayContentToMirror() { + defaultInit(); doReturn(null).when( mWm.mRoot).getDisplayContent(anyInt()); mContentRecorder.setContentRecordingSession(mDisplaySession); @@ -165,6 +183,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testUpdateRecording_task_nullToken() { + defaultInit(); ContentRecordingSession session = mTaskSession; session.setVirtualDisplayId(mDisplaySession.getVirtualDisplayId()); session.setTokenToRecord(null); @@ -176,6 +195,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testUpdateRecording_task_noWindowContainer() { + defaultInit(); // Use the window container token of the DisplayContent, rather than task. ContentRecordingSession invalidTaskSession = ContentRecordingSession.createTaskSession( new WindowContainer.RemoteToken(mDisplayContent)); @@ -187,6 +207,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testUpdateRecording_wasPaused() { + defaultInit(); mContentRecorder.setContentRecordingSession(mDisplaySession); mContentRecorder.updateRecording(); @@ -197,6 +218,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testUpdateRecording_waitingForConsent() { + defaultInit(); mContentRecorder.setContentRecordingSession(mWaitingDisplaySession); mContentRecorder.updateRecording(); assertThat(mContentRecorder.isCurrentlyRecording()).isFalse(); @@ -209,6 +231,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testOnConfigurationChanged_neverRecording() { + defaultInit(); mContentRecorder.onConfigurationChanged(ORIENTATION_PORTRAIT); verify(mTransaction, never()).setPosition(eq(mRecordedSurface), anyFloat(), anyFloat()); @@ -218,6 +241,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testOnConfigurationChanged_resizesSurface() { + defaultInit(); mContentRecorder.setContentRecordingSession(mDisplaySession); mContentRecorder.updateRecording(); // Ensure a different orientation when we check if something has changed. @@ -234,13 +258,14 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testOnConfigurationChanged_resizesVirtualDisplay() { + defaultInit(); final int newWidth = 55; mContentRecorder.setContentRecordingSession(mDisplaySession); mContentRecorder.updateRecording(); // The user rotates the device, so the host app resizes the virtual display for the capture. - resizeDisplay(mDisplayContent, newWidth, sSurfaceSize.y); - resizeDisplay(mVirtualDisplayContent, newWidth, sSurfaceSize.y); + resizeDisplay(mDisplayContent, newWidth, mSurfaceSize.y); + resizeDisplay(mVirtualDisplayContent, newWidth, mSurfaceSize.y); mContentRecorder.onConfigurationChanged(mDisplayContent.getConfiguration().orientation); verify(mTransaction, atLeast(2)).setPosition(eq(mRecordedSurface), anyFloat(), @@ -251,6 +276,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testOnConfigurationChanged_rotateVirtualDisplay() { + defaultInit(); mContentRecorder.setContentRecordingSession(mDisplaySession); mContentRecorder.updateRecording(); @@ -271,12 +297,13 @@ public class ContentRecorderTests extends WindowTestsBase { */ @Test public void testOnConfigurationChanged_resizeSurface() { + defaultInit(); mContentRecorder.setContentRecordingSession(mDisplaySession); mContentRecorder.updateRecording(); // Resize the output surface. - final Point newSurfaceSize = new Point(Math.round(sSurfaceSize.x / 2f), - Math.round(sSurfaceSize.y * 2)); + final Point newSurfaceSize = new Point(Math.round(mSurfaceSize.x / 2f), + Math.round(mSurfaceSize.y * 2)); doReturn(newSurfaceSize).when(mWm.mDisplayManagerInternal).getDisplaySurfaceDefaultSize( anyInt()); mContentRecorder.onConfigurationChanged( @@ -292,6 +319,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testOnTaskOrientationConfigurationChanged_resizesSurface() { + defaultInit(); mContentRecorder.setContentRecordingSession(mTaskSession); mContentRecorder.updateRecording(); @@ -314,6 +342,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testOnTaskBoundsConfigurationChanged_notifiesCallback() { + defaultInit(); mTask.getRootTask().setWindowingMode(WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW); final int minWidth = 222; @@ -351,6 +380,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testTaskWindowingModeChanged_pip_stopsRecording() { + defaultInit(); // WHEN a recording is ongoing. mTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN); mContentRecorder.setContentRecordingSession(mTaskSession); @@ -368,6 +398,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testTaskWindowingModeChanged_fullscreen_startsRecording() { + defaultInit(); // WHEN a recording is ongoing. mTask.setWindowingMode(WINDOWING_MODE_PINNED); mContentRecorder.setContentRecordingSession(mTaskSession); @@ -384,6 +415,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testStartRecording_notifiesCallback_taskSession() { + defaultInit(); // WHEN a recording is ongoing. mContentRecorder.setContentRecordingSession(mTaskSession); mContentRecorder.updateRecording(); @@ -396,6 +428,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testStartRecording_notifiesCallback_displaySession() { + defaultInit(); // WHEN a recording is ongoing. mContentRecorder.setContentRecordingSession(mDisplaySession); mContentRecorder.updateRecording(); @@ -408,6 +441,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testStartRecording_taskInPIP_recordingNotStarted() { + defaultInit(); // GIVEN a task is in PIP. mContentRecorder.setContentRecordingSession(mTaskSession); mTask.setWindowingMode(WINDOWING_MODE_PINNED); @@ -421,6 +455,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testStartRecording_taskInSplit_recordingStarted() { + defaultInit(); // GIVEN a task is in PIP. mContentRecorder.setContentRecordingSession(mTaskSession); mTask.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); @@ -434,6 +469,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testStartRecording_taskInFullscreen_recordingStarted() { + defaultInit(); // GIVEN a task is in PIP. mContentRecorder.setContentRecordingSession(mTaskSession); mTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN); @@ -447,6 +483,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testOnVisibleRequestedChanged_notifiesCallback() { + defaultInit(); // WHEN a recording is ongoing. mContentRecorder.setContentRecordingSession(mTaskSession); mContentRecorder.updateRecording(); @@ -471,6 +508,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testOnVisibleRequestedChanged_noRecording_doesNotNotifyCallback() { + defaultInit(); // WHEN a recording is not ongoing. assertThat(mContentRecorder.isCurrentlyRecording()).isFalse(); @@ -493,6 +531,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testPauseRecording_pausesRecording() { + defaultInit(); mContentRecorder.setContentRecordingSession(mDisplaySession); mContentRecorder.updateRecording(); @@ -502,12 +541,14 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testPauseRecording_neverRecording() { + defaultInit(); mContentRecorder.pauseRecording(); assertThat(mContentRecorder.isCurrentlyRecording()).isFalse(); } @Test public void testStopRecording_stopsRecording() { + defaultInit(); mContentRecorder.setContentRecordingSession(mDisplaySession); mContentRecorder.updateRecording(); @@ -517,12 +558,14 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testStopRecording_neverRecording() { + defaultInit(); mContentRecorder.stopRecording(); assertThat(mContentRecorder.isCurrentlyRecording()).isFalse(); } @Test public void testRemoveTask_stopsRecording() { + defaultInit(); mContentRecorder.setContentRecordingSession(mTaskSession); mContentRecorder.updateRecording(); @@ -533,6 +576,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testRemoveTask_stopsRecording_nullSessionShouldNotThrowExceptions() { + defaultInit(); mContentRecorder.setContentRecordingSession(mTaskSession); mContentRecorder.updateRecording(); mContentRecorder.setContentRecordingSession(null); @@ -541,6 +585,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testUpdateMirroredSurface_capturedAreaResized() { + defaultInit(); mContentRecorder.setContentRecordingSession(mDisplaySession); mContentRecorder.updateRecording(); assertThat(mContentRecorder.isCurrentlyRecording()).isTrue(); @@ -548,9 +593,9 @@ public class ContentRecorderTests extends WindowTestsBase { // WHEN attempting to mirror on the virtual display, and the captured content is resized. float xScale = 0.7f; float yScale = 2f; - Rect displayAreaBounds = new Rect(0, 0, Math.round(sSurfaceSize.x * xScale), - Math.round(sSurfaceSize.y * yScale)); - mContentRecorder.updateMirroredSurface(mTransaction, displayAreaBounds, sSurfaceSize); + Rect displayAreaBounds = new Rect(0, 0, Math.round(mSurfaceSize.x * xScale), + Math.round(mSurfaceSize.y * yScale)); + mContentRecorder.updateMirroredSurface(mTransaction, displayAreaBounds, mSurfaceSize); assertThat(mContentRecorder.isCurrentlyRecording()).isTrue(); // THEN content in the captured DisplayArea is scaled to fit the surface size. @@ -558,15 +603,139 @@ public class ContentRecorderTests extends WindowTestsBase { 1.0f / yScale); // THEN captured content is positioned in the centre of the output surface. int scaledWidth = Math.round((float) displayAreaBounds.width() / xScale); - int xInset = (sSurfaceSize.x - scaledWidth) / 2; + int xInset = (mSurfaceSize.x - scaledWidth) / 2; verify(mTransaction, atLeastOnce()).setPosition(mRecordedSurface, xInset, 0); // THEN the resize callback is notified. verify(mMediaProjectionManagerWrapper).notifyActiveProjectionCapturedContentResized( displayAreaBounds.width(), displayAreaBounds.height()); } + @Test + public void testUpdateMirroredSurface_isotropicPixel() { + mHandleAnisotropicDisplayMirroring = false; + DisplayInfo displayInfo = createDefaultDisplayInfo(); + createContentRecorder(displayInfo); + mContentRecorder.setContentRecordingSession(mDisplaySession); + mContentRecorder.updateRecording(); + assertThat(mContentRecorder.isCurrentlyRecording()).isTrue(); + + verify(mTransaction, atLeastOnce()).setMatrix(mRecordedSurface, 1, 0, 0, 1); + } + + @Test + public void testUpdateMirroredSurface_anisotropicPixel_compressY() { + mHandleAnisotropicDisplayMirroring = true; + DisplayInfo displayInfo = createDefaultDisplayInfo(); + DisplayInfo inputDisplayInfo = + mWm.mRoot.getDisplayContent(DEFAULT_DISPLAY).getDisplayInfo(); + displayInfo.physicalXDpi = 2.0f * inputDisplayInfo.physicalXDpi; + displayInfo.physicalYDpi = inputDisplayInfo.physicalYDpi; + createContentRecorder(displayInfo); + mContentRecorder.setContentRecordingSession(mDisplaySession); + mContentRecorder.updateRecording(); + assertThat(mContentRecorder.isCurrentlyRecording()).isTrue(); + + float xScale = 1f; + float yScale = 0.5f; + verify(mTransaction, atLeastOnce()).setMatrix(mRecordedSurface, xScale, 0, 0, + yScale); + verify(mTransaction, atLeastOnce()).setPosition(mRecordedSurface, 0, + Math.round(0.25 * mSurfaceSize.y)); + } + + @Test + public void testUpdateMirroredSurface_anisotropicPixel_compressX() { + mHandleAnisotropicDisplayMirroring = true; + DisplayInfo displayInfo = createDefaultDisplayInfo(); + DisplayInfo inputDisplayInfo = + mWm.mRoot.getDisplayContent(DEFAULT_DISPLAY).getDisplayInfo(); + displayInfo.physicalXDpi = inputDisplayInfo.physicalXDpi; + displayInfo.physicalYDpi = 2.0f * inputDisplayInfo.physicalYDpi; + createContentRecorder(displayInfo); + mContentRecorder.setContentRecordingSession(mDisplaySession); + mContentRecorder.updateRecording(); + assertThat(mContentRecorder.isCurrentlyRecording()).isTrue(); + + float xScale = 0.5f; + float yScale = 1f; + verify(mTransaction, atLeastOnce()).setMatrix(mRecordedSurface, xScale, 0, 0, + yScale); + verify(mTransaction, atLeastOnce()).setPosition(mRecordedSurface, + Math.round(0.25 * mSurfaceSize.x), 0); + } + + @Test + public void testUpdateMirroredSurface_anisotropicPixel_scaleOnX() { + mHandleAnisotropicDisplayMirroring = true; + int width = 2 * mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().width(); + int height = 6 * mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().height(); + DisplayInfo displayInfo = createDisplayInfo(width, height); + DisplayInfo inputDisplayInfo = + mWm.mRoot.getDisplayContent(DEFAULT_DISPLAY).getDisplayInfo(); + displayInfo.physicalXDpi = inputDisplayInfo.physicalXDpi; + displayInfo.physicalYDpi = 2.0f * inputDisplayInfo.physicalYDpi; + createContentRecorder(displayInfo); + mContentRecorder.setContentRecordingSession(mDisplaySession); + mContentRecorder.updateRecording(); + assertThat(mContentRecorder.isCurrentlyRecording()).isTrue(); + + float xScale = 2f; + float yScale = 4f; + verify(mTransaction, atLeastOnce()).setMatrix(mRecordedSurface, xScale, 0, 0, + yScale); + verify(mTransaction, atLeastOnce()).setPosition(mRecordedSurface, 0, + inputDisplayInfo.logicalHeight); + } + + @Test + public void testUpdateMirroredSurface_anisotropicPixel_scaleOnY() { + mHandleAnisotropicDisplayMirroring = true; + int width = 6 * mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().width(); + int height = 2 * mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().height(); + DisplayInfo displayInfo = createDisplayInfo(width, height); + DisplayInfo inputDisplayInfo = + mWm.mRoot.getDisplayContent(DEFAULT_DISPLAY).getDisplayInfo(); + displayInfo.physicalXDpi = 2.0f * inputDisplayInfo.physicalXDpi; + displayInfo.physicalYDpi = inputDisplayInfo.physicalYDpi; + createContentRecorder(displayInfo); + mContentRecorder.setContentRecordingSession(mDisplaySession); + mContentRecorder.updateRecording(); + assertThat(mContentRecorder.isCurrentlyRecording()).isTrue(); + + float xScale = 4f; + float yScale = 2f; + verify(mTransaction, atLeastOnce()).setMatrix(mRecordedSurface, xScale, 0, 0, + yScale); + verify(mTransaction, atLeastOnce()).setPosition(mRecordedSurface, + inputDisplayInfo.logicalWidth, 0); + } + + @Test + public void testUpdateMirroredSurface_anisotropicPixel_shrinkCanvas() { + mHandleAnisotropicDisplayMirroring = true; + int width = mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().width() / 2; + int height = mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().height() / 2; + DisplayInfo displayInfo = createDisplayInfo(width, height); + DisplayInfo inputDisplayInfo = + mWm.mRoot.getDisplayContent(DEFAULT_DISPLAY).getDisplayInfo(); + displayInfo.physicalXDpi = 2f * inputDisplayInfo.physicalXDpi; + displayInfo.physicalYDpi = inputDisplayInfo.physicalYDpi; + createContentRecorder(displayInfo); + mContentRecorder.setContentRecordingSession(mDisplaySession); + mContentRecorder.updateRecording(); + assertThat(mContentRecorder.isCurrentlyRecording()).isTrue(); + + float xScale = 0.5f; + float yScale = 0.25f; + verify(mTransaction, atLeastOnce()).setMatrix(mRecordedSurface, xScale, 0, 0, + yScale); + verify(mTransaction, atLeastOnce()).setPosition(mRecordedSurface, 0, + (mSurfaceSize.y - height / 2) / 2); + } + @Test public void testDisplayContentUpdatesRecording_withoutSurface() { + defaultInit(); // GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to // mirror. setUpDefaultTaskDisplayAreaWindowToken(); @@ -585,6 +754,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testDisplayContentUpdatesRecording_withSurface() { + defaultInit(); // GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to // mirror. setUpDefaultTaskDisplayAreaWindowToken(); @@ -602,12 +772,13 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testDisplayContentUpdatesRecording_displayMirroring() { + defaultInit(); // GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to // mirror. setUpDefaultTaskDisplayAreaWindowToken(); // GIVEN SurfaceControl can successfully mirror the provided surface. - surfaceControlMirrors(sSurfaceSize); + surfaceControlMirrors(mSurfaceSize); // Initially disable getDisplayIdToMirror since the DMS may create the DC outside the direct // call in the test. We need to spy on the DC before updateRecording is called or we can't // verify setDisplayMirroring is called -- cgit v1.2.3-59-g8ed1b From fcbcaaded9449e79a60942dfc667dea166ba773d Mon Sep 17 00:00:00 2001 From: Evan Rosky Date: Wed, 18 Oct 2023 16:55:00 -0700 Subject: Add base impl of new ready tracker This introduces a new ReadyTracker class based around tracking explicit conditions instead of an arbitrary set of heuristics mixed with legacy. Usage involves constructing a ReadyCondition and adding it to a transition's tracker. Once the condition is met, call `meet()` on the condition. The new ReadyTracker considers the transition ready if there was at-least one condition added and all the added conditions have been met. This is just the base implementation so it isn't really functional yet. This does add ready-conditions in most of the places where the old method's defer/continue were used. Bug: 294925498 Test: atest TransitionTests Change-Id: I6cbd9152d03e707e0e7134fa8ada2902e5af847e --- data/etc/services.core.protolog.json | 18 +++ .../java/com/android/server/wm/DisplayContent.java | 10 ++ .../com/android/server/wm/KeyguardController.java | 16 +- .../com/android/server/wm/RootWindowContainer.java | 3 + services/core/java/com/android/server/wm/Task.java | 16 +- .../java/com/android/server/wm/Transition.java | 171 ++++++++++++++++++++- .../android/server/wm/TransitionController.java | 24 +++ .../server/wm/WindowOrganizerController.java | 22 +++ .../src/com/android/server/wm/TransitionTests.java | 40 +++++ 9 files changed, 312 insertions(+), 8 deletions(-) (limited to 'data') diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index b71aaf3fc2e6..fefe5be57615 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -2365,6 +2365,12 @@ "group": "WM_DEBUG_RECENTS_ANIMATIONS", "at": "com\/android\/server\/wm\/RecentsAnimationController.java" }, + "33989965": { + "message": " Met condition %s for #%d (%d left)", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "34106798": { "message": "Content Recording: Display %d state was (%d), is now (%d), so update recording?", "level": "VERBOSE", @@ -4483,6 +4489,12 @@ "group": "WM_DEBUG_APP_TRANSITIONS", "at": "com\/android\/server\/wm\/AppTransitionController.java" }, + "2053743391": { + "message": " Add condition %s for #%d", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "2060978050": { "message": "moveWindowTokenToDisplay: Attempted to move token: %s to non-exiting displayId=%d", "level": "WARN", @@ -4543,6 +4555,12 @@ "group": "WM_DEBUG_IME", "at": "com\/android\/server\/wm\/DisplayContent.java" }, + "2124732293": { + "message": "#%d: Met condition: %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "2128917433": { "message": "onProposedRotationChanged, rotation=%d", "level": "VERBOSE", diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index b7b5c2af0e3e..a85f2d6876b5 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1621,7 +1621,17 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return; } + final Transition.ReadyCondition displayConfig = mTransitionController.isCollecting() + ? new Transition.ReadyCondition("displayConfig", this) : null; + if (displayConfig != null) { + mTransitionController.waitFor(displayConfig); + } else if (mTransitionController.isShellTransitionsEnabled()) { + Slog.e(TAG, "Display reconfigured outside of a transition: " + this); + } final boolean configUpdated = updateDisplayOverrideConfigurationLocked(); + if (displayConfig != null) { + displayConfig.meet(); + } if (configUpdated) { return; } diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java index 1a319ad61116..fa2c94a1ecf2 100644 --- a/services/core/java/com/android/server/wm/KeyguardController.java +++ b/services/core/java/com/android/server/wm/KeyguardController.java @@ -87,6 +87,7 @@ class KeyguardController { private RootWindowContainer mRootWindowContainer; private final ActivityTaskManagerInternal.SleepTokenAcquirer mSleepTokenAcquirer; private boolean mWaitingForWakeTransition; + private Transition.ReadyCondition mWaitAodHide = null; KeyguardController(ActivityTaskManagerService service, ActivityTaskSupervisor taskSupervisor) { @@ -565,8 +566,14 @@ class KeyguardController { // Defer transition until AOD dismissed. void updateDeferTransitionForAod(boolean waiting) { - if (waiting == mWaitingForWakeTransition) { - return; + if (mService.getTransitionController().useFullReadyTracking()) { + if (waiting == (mWaitAodHide != null)) { + return; + } + } else { + if (waiting == mWaitingForWakeTransition) { + return; + } } if (!mService.getTransitionController().isCollecting()) { return; @@ -575,12 +582,17 @@ class KeyguardController { if (waiting && isAodShowing(DEFAULT_DISPLAY)) { mWaitingForWakeTransition = true; mWindowManager.mAtmService.getTransitionController().deferTransitionReady(); + mWaitAodHide = new Transition.ReadyCondition("AOD hidden"); + mWindowManager.mAtmService.getTransitionController().waitFor(mWaitAodHide); mWindowManager.mH.postDelayed(mResetWaitTransition, DEFER_WAKE_TRANSITION_TIMEOUT_MS); } else if (!waiting) { // dismiss the deferring if the AOD state change or cancel awake. mWaitingForWakeTransition = false; mWindowManager.mAtmService.getTransitionController().continueTransitionReady(); mWindowManager.mH.removeCallbacks(mResetWaitTransition); + final Transition.ReadyCondition waitAodHide = mWaitAodHide; + mWaitAodHide = null; + waitAodHide.meet(); } } diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 7a442e708130..fcc4ab606d1b 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -2039,6 +2039,8 @@ class RootWindowContainer extends WindowContainer } transitionController.deferTransitionReady(); + Transition.ReadyCondition pipChangesApplied = new Transition.ReadyCondition("movedToPip"); + transitionController.waitFor(pipChangesApplied); mService.deferWindowLayout(); try { // This will change the root pinned task's windowing mode to its original mode, ensuring @@ -2223,6 +2225,7 @@ class RootWindowContainer extends WindowContainer } } finally { transitionController.continueTransitionReady(); + pipChangesApplied.meet(); } } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 4922e9028ed9..04022e8096c7 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -4725,7 +4725,7 @@ class Task extends TaskFragment { } if (isAttached()) { setWindowingMode(WINDOWING_MODE_UNDEFINED); - moveTaskToBackInner(this); + moveTaskToBackInner(this, null /* transition */); } if (top.isAttached()) { top.setWindowingMode(WINDOWING_MODE_UNDEFINED); @@ -5728,24 +5728,27 @@ class Task extends TaskFragment { mTransitionController.requestStartTransition(transition, tr, null /* remoteTransition */, null /* displayChange */); mTransitionController.collect(tr); - moveTaskToBackInner(tr); + moveTaskToBackInner(tr, transition); }); } else { // Skip the transition for pinned task. if (!inPinnedWindowingMode()) { mDisplayContent.prepareAppTransition(TRANSIT_TO_BACK); } - moveTaskToBackInner(tr); + moveTaskToBackInner(tr, null /* transition */); } return true; } - private boolean moveTaskToBackInner(@NonNull Task task) { - if (mTransitionController.isShellTransitionsEnabled()) { + private boolean moveTaskToBackInner(@NonNull Task task, @Nullable Transition transition) { + final Transition.ReadyCondition movedToBack = + new Transition.ReadyCondition("moved-to-back", task); + if (transition != null) { // Preventing from update surface position for WindowState if configuration changed, // because the position is depends on WindowFrame, so update the position before // relayout will only update it to "old" position. mAtmService.deferWindowLayout(); + transition.mReadyTracker.add(movedToBack); } try { moveToBack("moveTaskToBackInner", task); @@ -5762,6 +5765,9 @@ class Task extends TaskFragment { if (mTransitionController.isShellTransitionsEnabled()) { mAtmService.continueWindowLayout(); } + if (transition != null) { + movedToBack.meet(); + } } ActivityRecord topActivity = getDisplayArea().topRunningActivity(); Task topRootTask = topActivity == null ? null : topActivity.getRootTask(); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index ff04945ad6df..f6b4972de250 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -237,6 +237,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { private @TransitionState int mState = STATE_PENDING; private final ReadyTrackerOld mReadyTrackerOld = new ReadyTrackerOld(); + final ReadyTracker mReadyTracker = new ReadyTracker(this); private int mRecentsDisplayId = INVALID_DISPLAY; @@ -893,7 +894,12 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { private void applyReady() { if (mState < STATE_STARTED) return; - final boolean ready = mReadyTrackerOld.allReady(); + final boolean ready; + if (mController.useFullReadyTracking()) { + ready = mReadyTracker.isReady(); + } else { + ready = mReadyTrackerOld.allReady(); + } ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Set transition ready=%b %d", ready, mSyncId); boolean changed = mSyncEngine.setReady(mSyncId, ready); @@ -1438,6 +1444,13 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Force Playing Transition: %d", mSyncId); mForcePlaying = true; + // backwards since conditions are removed. + for (int i = mReadyTracker.mConditions.size() - 1; i >= 0; --i) { + mReadyTracker.mConditions.get(i).meetAlternate("play-now"); + } + final ReadyCondition forcePlay = new ReadyCondition("force-play-now"); + mReadyTracker.add(forcePlay); + forcePlay.meet(); setAllReady(); if (mState == STATE_COLLECTING) { start(); @@ -1483,6 +1496,21 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { return; } + if (mController.useFullReadyTracking()) { + if (mReadyTracker.mMet.isEmpty()) { + Slog.e(TAG, "#" + mSyncId + ": No conditions provided"); + } else { + for (int i = 0; i < mReadyTracker.mConditions.size(); ++i) { + Slog.e(TAG, "#" + mSyncId + ": unmet condition at ready: " + + mReadyTracker.mConditions.get(i)); + } + } + for (int i = 0; i < mReadyTracker.mMet.size(); ++i) { + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "#%d: Met condition: %s", + mSyncId, mReadyTracker.mMet.get(i)); + } + } + // Commit the visibility of visible activities before calculateTransitionInfo(), so the // TaskInfo can be visible. Also it needs to be done before moveToPlaying(), otherwise // ActivityRecord#canShowWindows() may reject to show its window. The visibility also @@ -3071,6 +3099,147 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { applyReady(); } + /** + * Represents a condition that must be met before an associated transition can be considered + * ready. + * + * Expected usage is that a ReadyCondition is created and then attached to a transition's + * ReadyTracker via {@link ReadyTracker#add}. After that, it is expected to monitor the state + * of the system and when the condition it represents is met, it will call + * {@link ReadyTracker#meet}. + * + * This base class is a simple explicit, named condition. A caller will create/attach the + * condition and then explicitly call {@link #meet} on it (which internally calls + * {@link ReadyTracker#meet}. + * + * Example: + *

+     *     ReadyCondition myCondition = new ReadyCondition("my condition");
+     *     transitionController.waitFor(myCondition);
+     *     ... Some operations ...
+     *     myCondition.meet();
+     * 
+ */ + static class ReadyCondition { + final String mName; + + /** Just used for debugging */ + final Object mDebugTarget; + ReadyTracker mTracker; + boolean mMet = false; + + /** If set (non-null), then this is met by another reason besides state (eg. timeout). */ + String mAlternate = null; + + ReadyCondition(@NonNull String name) { + mName = name; + mDebugTarget = null; + } + + ReadyCondition(@NonNull String name, @Nullable Object debugTarget) { + mName = name; + mDebugTarget = debugTarget; + } + + protected String getDebugRep() { + if (mDebugTarget != null) { + return mName + ":" + mDebugTarget; + } + return mName; + } + + @Override + public String toString() { + return "{" + getDebugRep() + (mAlternate != null ? " (" + mAlternate + ")" : "") + "}"; + } + + /** + * Instructs this condition to start tracking system state to detect when this is met. + * Don't call this directly; it is called when this object is attached to a transition's + * ready-tracker. + */ + void startTracking() { + } + + /** + * Immediately consider this condition met by an alternative reason (one which doesn't + * match the normal intent of this condition -- eg. a timeout). + */ + void meetAlternate(@NonNull String reason) { + if (mMet) return; + mAlternate = reason; + meet(); + } + + /** Immediately consider this condition met. */ + void meet() { + if (mMet) return; + if (mTracker == null) { + throw new IllegalStateException("Can't meet a condition before it is waited on"); + } + mTracker.meet(this); + } + } + + static class ReadyTracker { + /** + * Used as a place-holder in situations where the transition system isn't active (such as + * early-boot, mid shell crash/recovery, or when using legacy). + */ + static final ReadyTracker NULL_TRACKER = new ReadyTracker(null); + + private final Transition mTransition; + + /** List of conditions that are still being waited on. */ + final ArrayList mConditions = new ArrayList<>(); + + /** List of already-met conditions. Fully-qualified for debugging. */ + final ArrayList mMet = new ArrayList<>(); + + ReadyTracker(Transition transition) { + mTransition = transition; + } + + void add(@NonNull ReadyCondition condition) { + if (mTransition == null || !mTransition.mController.useFullReadyTracking()) { + condition.mTracker = NULL_TRACKER; + return; + } + mConditions.add(condition); + condition.mTracker = this; + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Add condition %s for #%d", + condition, mTransition.mSyncId); + condition.startTracking(); + } + + void meet(@NonNull ReadyCondition condition) { + if (mTransition == null || !mTransition.mController.useFullReadyTracking()) return; + if (mTransition.mState >= STATE_PLAYING) { + Slog.w(TAG, "#%d: Condition met too late, already in state=" + mTransition.mState + + ": " + condition); + return; + } + if (!mConditions.remove(condition)) { + if (mMet.contains(condition)) { + throw new IllegalStateException("Can't meet the same condition more than once: " + + condition + " #" + mTransition.mSyncId); + } else { + throw new IllegalArgumentException("Can't meet a condition that isn't being " + + "waited on: " + condition + " in #" + mTransition.mSyncId); + } + } + condition.mMet = true; + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Met condition %s for #%d (%d" + + " left)", condition, mTransition.mSyncId, mConditions.size()); + mMet.add(condition); + mTransition.applyReady(); + } + + boolean isReady() { + return mConditions.isEmpty() && !mMet.isEmpty(); + } + } + /** * The transition sync mechanism has 2 parts: * 1. Whether all WM operations for a particular transition are "ready" (eg. did the app diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 8ac21e41f7f4..28c24c82e830 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -62,6 +62,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLogGroup; import com.android.internal.protolog.common.ProtoLog; import com.android.server.FgThread; +import com.android.window.flags.Flags; import java.util.ArrayList; import java.util.function.Consumer; @@ -131,6 +132,7 @@ class TransitionController { TransitionTracer mTransitionTracer; private SystemPerformanceHinter mSystemPerformanceHinter; + private boolean mFullReadyTracking = false; private final ArrayList mLegacyListeners = new ArrayList<>(); @@ -281,6 +283,7 @@ class TransitionController { registerLegacyListener(wms.mActivityManagerAppTransitionNotifier); setSyncEngine(wms.mSyncEngine); setSystemPerformanceHinter(wms.mSystemPerformanceHinter); + mFullReadyTracking = Flags.transitReadyTracking(); } @VisibleForTesting @@ -397,6 +400,14 @@ class TransitionController { return isShellTransitionsEnabled() && SHELL_TRANSITIONS_ROTATION; } + boolean useFullReadyTracking() { + return mFullReadyTracking; + } + + void setFullReadyTrackingForTest(boolean enabled) { + mFullReadyTracking = enabled; + } + /** * @return {@code true} if transition is actively collecting changes. This is {@code false} * once a transition is playing @@ -1475,6 +1486,19 @@ class TransitionController { applySync.accept(false /* deferred */); } + /** + * Instructs the collecting transition to wait until `condition` is met before it is + * considered ready. + */ + void waitFor(@NonNull Transition.ReadyCondition condition) { + if (mCollectingTransition == null) { + Slog.e(TAG, "No collecting transition available to wait for " + condition); + condition.mTracker = Transition.ReadyTracker.NULL_TRACKER; + return; + } + mCollectingTransition.mReadyTracker.add(condition); + } + interface OnStartCollect { void onCollectStarted(boolean deferred); } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 5ed6caffe1fb..51a0ccafcefc 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -305,9 +305,12 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub // This is a direct call from shell, so the entire transition lifecycle is // contained in the provided transaction if provided. Thus, we can setReady // immediately after apply. + final Transition.ReadyCondition wctApplied = + new Transition.ReadyCondition("start WCT applied"); final boolean needsSetReady = t != null; final Transition nextTransition = new Transition(type, 0 /* flags */, mTransitionController, mService.mWindowManager.mSyncEngine); + nextTransition.mReadyTracker.add(wctApplied); nextTransition.calcParallelCollectType(wct); mTransitionController.startCollectOrQueue(nextTransition, (deferred) -> { @@ -315,6 +318,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub nextTransition.mLogger.mStartWCT = wct; applyTransaction(wct, -1 /* syncId */, nextTransition, caller, deferred); + wctApplied.meet(); if (needsSetReady) { // TODO(b/294925498): Remove this once we have accurate ready // tracking. @@ -329,6 +333,15 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub }); return nextTransition.getToken(); } + // Currently, application of wct can span multiple looper loops (ie. + // waitAsyncStart), so add a condition to ensure that it finishes applying. + final Transition.ReadyCondition wctApplied; + if (t != null) { + wctApplied = new Transition.ReadyCondition("start WCT applied"); + transition.mReadyTracker.add(wctApplied); + } else { + wctApplied = null; + } // The transition already started collecting before sending a request to shell, // so just start here. if (!transition.isCollecting() && !transition.isForcePlaying()) { @@ -336,6 +349,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub + " means Shell took too long to respond to a request. WM State may be" + " incorrect now, please file a bug"); applyTransaction(wct, -1 /*syncId*/, null /*transition*/, caller); + if (wctApplied != null) { + wctApplied.meet(); + } return transition.getToken(); } transition.mLogger.mStartWCT = wct; @@ -344,11 +360,17 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub synchronized (mService.mGlobalLock) { transition.start(); applyTransaction(wct, -1 /* syncId */, transition, caller); + if (wctApplied != null) { + wctApplied.meet(); + } } }); } else { transition.start(); applyTransaction(wct, -1 /* syncId */, transition, caller); + if (wctApplied != null) { + wctApplied.meet(); + } } // Since the transition is already provided, it means WMCore is determining the // "readiness lifecycle" outside the provided transaction, so don't set ready here. diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index c8546c613995..47730237f675 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -2475,6 +2475,46 @@ public class TransitionTests extends WindowTestsBase { verify(session).close(); } + @Test + public void testReadyTrackerBasics() { + final TransitionController controller = new TestTransitionController( + mock(ActivityTaskManagerService.class)); + controller.setFullReadyTrackingForTest(true); + Transition transit = createTestTransition(TRANSIT_OPEN, controller); + // Not ready if nothing has happened yet + assertFalse(transit.mReadyTracker.isReady()); + + Transition.ReadyCondition condition1 = new Transition.ReadyCondition("c1"); + transit.mReadyTracker.add(condition1); + assertFalse(transit.mReadyTracker.isReady()); + + Transition.ReadyCondition condition2 = new Transition.ReadyCondition("c2"); + transit.mReadyTracker.add(condition2); + assertFalse(transit.mReadyTracker.isReady()); + + condition2.meet(); + assertFalse(transit.mReadyTracker.isReady()); + + condition1.meet(); + assertTrue(transit.mReadyTracker.isReady()); + } + + @Test + public void testReadyTrackerAlternate() { + final TransitionController controller = new TestTransitionController( + mock(ActivityTaskManagerService.class)); + controller.setFullReadyTrackingForTest(true); + Transition transit = createTestTransition(TRANSIT_OPEN, controller); + + Transition.ReadyCondition condition1 = new Transition.ReadyCondition("c1"); + transit.mReadyTracker.add(condition1); + assertFalse(transit.mReadyTracker.isReady()); + + condition1.meetAlternate("reason1"); + assertTrue(transit.mReadyTracker.isReady()); + assertEquals("reason1", condition1.mAlternate); + } + private static void makeTaskOrganized(Task... tasks) { final ITaskOrganizer organizer = mock(ITaskOrganizer.class); for (Task t : tasks) { -- cgit v1.2.3-59-g8ed1b From 4fe9c2c3db49bd846fe383258951b9f560e62b6e Mon Sep 17 00:00:00 2001 From: Nick Chameyev Date: Fri, 20 Oct 2023 12:29:22 +0000 Subject: Take over animating of unfold Shell transitions Adds logic to UnfoldTransitionHandler that takes over animating a transition if it contains changes related to unfold. Bug: 259220649 Test: atest PhysicalDisplaySwitchTransitionLauncherMixedTest Test: manual unfold 20 times => verify that there is no black screen delay Test: artificially increase collecting of rotation transition => rotate and unfold device, check that animation works Flag: handle_mixed_unfold_transitions Change-Id: I3285c1b6a45da63ac7846e9d5929db9426b069ae --- data/etc/services.core.protolog.json | 6 ++ .../wm/shell/unfold/UnfoldTransitionHandler.java | 40 ++++++++- .../shell/unfold/UnfoldTransitionHandlerTest.java | 95 ++++++++++++++++++++++ .../PhysicalDisplaySwitchTransitionLauncher.java | 29 +++++-- ...hysicalDisplaySwitchTransitionLauncherTest.java | 29 ++++++- 5 files changed, 189 insertions(+), 10 deletions(-) (limited to 'data') diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index cc73ecee2146..9186777dc56d 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -961,6 +961,12 @@ "group": "WM_DEBUG_RECENTS_ANIMATIONS", "at": "com\/android\/server\/wm\/RecentsAnimationController.java" }, + "-1204565480": { + "message": "Adding display switch to existing collecting transition", + "level": "DEBUG", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/PhysicalDisplaySwitchTransitionLauncher.java" + }, "-1198579104": { "message": "Pushing next activity %s out to target's task %s", "level": "VERBOSE", diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java index 68b5a81f8d7b..b26d0613f3b6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java @@ -20,6 +20,7 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TRANSITIONS; +import android.animation.ValueAnimator; import android.app.ActivityManager; import android.os.IBinder; import android.view.SurfaceControl; @@ -74,9 +75,9 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene Executor executor, Transitions transitions) { mUnfoldProgressProvider = unfoldProgressProvider; + mTransitions = transitions; mTransactionPool = transactionPool; mExecutor = executor; - mTransitions = transitions; mAnimators.add(splitUnfoldTaskAnimator); mAnimators.add(fullscreenUnfoldAnimator); @@ -104,6 +105,16 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull TransitionFinishCallback finishCallback) { + if (hasUnfold(info) && transition != mTransition) { + // Take over transition that has unfold, we might receive it if no other handler + // accepted request in handleRequest, e.g. for rotation + unfold or + // TRANSIT_NONE + unfold transitions + mTransition = transition; + + ProtoLog.v(WM_SHELL_TRANSITIONS, "UnfoldTransitionHandler: " + + "take over startAnimation"); + } + if (transition != mTransition) return false; for (int i = 0; i < mAnimators.size(); i++) { @@ -203,6 +214,33 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene && request.getDisplayChange().isPhysicalDisplayChanged()); } + /** Whether `transitionInfo` contains an unfold action. */ + public boolean hasUnfold(@NonNull TransitionInfo transitionInfo) { + // Unfold animation won't play when animations are disabled + if (!ValueAnimator.areAnimatorsEnabled()) return false; + + for (int i = 0; i < transitionInfo.getChanges().size(); i++) { + final TransitionInfo.Change change = transitionInfo.getChanges().get(i); + if ((change.getFlags() & TransitionInfo.FLAG_IS_DISPLAY) != 0) { + if (change.getEndAbsBounds() == null || change.getStartAbsBounds() == null) { + continue; + } + + // Handle only unfolding, currently we don't have an animation when folding + final int afterArea = + change.getEndAbsBounds().width() * change.getEndAbsBounds().height(); + final int beforeArea = change.getStartAbsBounds().width() + * change.getStartAbsBounds().height(); + + if (afterArea > beforeArea) { + return true; + } + } + } + + return false; + } + @Nullable @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java index 1d94c9c31de2..7917ba6eeb41 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java @@ -17,9 +17,12 @@ package com.android.wm.shell.unfold; import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_NONE; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; @@ -27,6 +30,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.app.ActivityManager; +import android.graphics.Rect; import android.os.Binder; import android.os.IBinder; import android.view.Display; @@ -35,6 +39,9 @@ import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; +import com.android.window.flags.FakeFeatureFlagsImpl; +import com.android.window.flags.FeatureFlags; +import com.android.window.flags.Flags; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.sysui.ShellInit; @@ -45,6 +52,8 @@ import com.android.wm.shell.unfold.animation.SplitTaskUnfoldAnimator; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import java.util.ArrayList; import java.util.List; @@ -131,6 +140,55 @@ public class UnfoldTransitionHandlerTest { verify(finishCallback, never()).onTransitionFinished(any()); } + @Test + public void startAnimation_sameTransitionAsHandleRequest_startsAnimation() { + TransitionRequestInfo requestInfo = createUnfoldTransitionRequestInfo(); + mUnfoldTransitionHandler.handleRequest(mTransition, requestInfo); + TransitionFinishCallback finishCallback = mock(TransitionFinishCallback.class); + + boolean animationStarted = mUnfoldTransitionHandler.startAnimation( + mTransition, + mock(TransitionInfo.class), + mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), + finishCallback + ); + + assertThat(animationStarted).isTrue(); + } + + @Test + public void startAnimation_differentTransitionFromRequestWithUnfold_startsAnimation() { + mUnfoldTransitionHandler.handleRequest(new Binder(), createNoneTransitionInfo()); + TransitionFinishCallback finishCallback = mock(TransitionFinishCallback.class); + + boolean animationStarted = mUnfoldTransitionHandler.startAnimation( + mTransition, + createUnfoldTransitionInfo(), + mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), + finishCallback + ); + + assertThat(animationStarted).isTrue(); + } + + @Test + public void startAnimation_differentTransitionFromRequestWithoutUnfold_doesNotStart() { + mUnfoldTransitionHandler.handleRequest(new Binder(), createNoneTransitionInfo()); + TransitionFinishCallback finishCallback = mock(TransitionFinishCallback.class); + + boolean animationStarted = mUnfoldTransitionHandler.startAnimation( + mTransition, + createNonUnfoldTransitionInfo(), + mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), + finishCallback + ); + + assertThat(animationStarted).isFalse(); + } + @Test public void startAnimation_animationFinishes_finishesTheTransition() { TransitionRequestInfo requestInfo = createUnfoldTransitionRequestInfo(); @@ -215,6 +273,12 @@ public class UnfoldTransitionHandlerTest { triggerTaskInfo, /* remoteTransition= */ null, displayChange, 0 /* flags */); } + private TransitionRequestInfo createNoneTransitionInfo() { + return new TransitionRequestInfo(TRANSIT_NONE, + /* triggerTask= */ null, /* remoteTransition= */ null, + /* displayChange= */ null, /* flags= */ 0); + } + private static class TestShellUnfoldProgressProvider implements ShellUnfoldProgressProvider, ShellUnfoldProgressProvider.UnfoldListener { @@ -277,4 +341,35 @@ public class UnfoldTransitionHandlerTest { return false; } } + + static class TestCase { + private final boolean mShouldHandleMixedUnfold; + + public TestCase(boolean shouldHandleMixedUnfold) { + mShouldHandleMixedUnfold = shouldHandleMixedUnfold; + } + + public boolean mixedUnfoldFlagEnabled() { + return mShouldHandleMixedUnfold; + } + + @Override + public String toString() { + return "shouldHandleMixedUnfold flag = " + mShouldHandleMixedUnfold; + } + } + + private TransitionInfo createUnfoldTransitionInfo() { + TransitionInfo transitionInfo = new TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0); + TransitionInfo.Change change = new TransitionInfo.Change(null, mock(SurfaceControl.class)); + change.setStartAbsBounds(new Rect(0, 0, 10, 10)); + change.setEndAbsBounds(new Rect(0, 0, 100, 100)); + change.setFlags(TransitionInfo.FLAG_IS_DISPLAY); + transitionInfo.addChange(change); + return transitionInfo; + } + + private TransitionInfo createNonUnfoldTransitionInfo() { + return new TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0); + } } \ No newline at end of file diff --git a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java index 5f4a1c5ca32c..0d15fc932e68 100644 --- a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java +++ b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java @@ -34,6 +34,8 @@ import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.ProtoLogGroup; +import com.android.internal.protolog.common.ProtoLog; import com.android.server.wm.DeviceStateController.DeviceState; public class PhysicalDisplaySwitchTransitionLauncher { @@ -117,14 +119,27 @@ public class PhysicalDisplaySwitchTransitionLauncher { displayChange.setEndAbsBounds(endAbsBounds); displayChange.setPhysicalDisplayChanged(true); - final Transition t = mTransitionController.requestTransitionIfNeeded(TRANSIT_CHANGE, - 0 /* flags */, - mDisplayContent, mDisplayContent, null /* remoteTransition */, - displayChange); + mTransition = null; + + if (mTransitionController.isCollecting()) { + // Add display container to the currently collecting transition + mTransitionController.collect(mDisplayContent); + mTransition = mTransitionController.getCollectingTransition(); + + // Make sure that transition is not ready until we finish the remote display change + mTransition.setReady(mDisplayContent, false); + + ProtoLog.d(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + "Adding display switch to existing collecting transition"); + } else { + mTransition = mTransitionController.requestTransitionIfNeeded(TRANSIT_CHANGE, + 0 /* flags */, + mDisplayContent, mDisplayContent, null /* remoteTransition */, + displayChange); + } - if (t != null) { - mDisplayContent.mAtmService.startPowerMode(POWER_MODE_REASON_CHANGE_DISPLAY); - mTransition = t; + if (mTransition != null) { + mAtmService.startPowerMode(POWER_MODE_REASON_CHANGE_DISPLAY); } mShouldRequestTransitionOnDisplaySwitch = false; diff --git a/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java b/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java index cc8dab99c818..08e63963c88f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java @@ -34,6 +34,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.animation.ValueAnimator; +import android.annotation.Nullable; import android.content.Context; import android.content.res.Resources; import android.graphics.Rect; @@ -61,8 +62,7 @@ import org.mockito.MockitoAnnotations; */ @SmallTest @Presubmit -@RunWith(WindowTestRunner.class) -public class PhysicalDisplaySwitchTransitionLauncherTest extends WindowTestsBase { +public class PhysicalDisplaySwitchTransitionLauncherTest { @Mock DisplayContent mDisplayContent; @@ -73,6 +73,8 @@ public class PhysicalDisplaySwitchTransitionLauncherTest extends WindowTestsBase @Mock ActivityTaskManagerService mActivityTaskManagerService; @Mock + BLASTSyncEngine mSyncEngine; + @Mock TransitionController mTransitionController; private PhysicalDisplaySwitchTransitionLauncher mTarget; @@ -216,6 +218,20 @@ public class PhysicalDisplaySwitchTransitionLauncherTest extends WindowTestsBase assertTransitionNotRequested(); } + @Test + public void testDisplaySwitchAfterUnfolding_otherCollectingTransition_collectsDisplaySwitch() { + givenCollectingTransition(createTransition(TRANSIT_CHANGE)); + givenAllAnimationsEnabled(); + mTarget.foldStateChanged(FOLDED); + + mTarget.foldStateChanged(OPEN); + requestDisplaySwitch(); + + // Collects to the current transition + verify(mTransitionController).collect(mDisplayContent); + } + + @Test public void testDisplaySwitch_whenNoContentInDisplayContent_noTransition() { givenAllAnimationsEnabled(); @@ -267,6 +283,15 @@ public class PhysicalDisplaySwitchTransitionLauncherTest extends WindowTestsBase when(mTransitionController.isShellTransitionsEnabled()).thenReturn(enabled); } + private void givenCollectingTransition(@Nullable Transition transition) { + when(mTransitionController.isCollecting()).thenReturn(transition != null); + when(mTransitionController.getCollectingTransition()).thenReturn(transition); + } + + private Transition createTransition(int type) { + return new Transition(type, /* flags= */ 0, mTransitionController, mSyncEngine); + } + private void givenDisplayContentHasContent(boolean hasContent) { when(mDisplayContent.getLastHasContent()).thenReturn(hasContent); } -- cgit v1.2.3-59-g8ed1b From 943ec2368e5fe6f3ceea8035fd21728b4089e6d9 Mon Sep 17 00:00:00 2001 From: Zyan Wu Date: Mon, 30 Oct 2023 11:55:46 +0800 Subject: Modify which keys of D6 can turn on screen of ATV. Test: None Bug: 308352352 Doc: https://docs.google.com/document/d/15MiUQNkH2cXjrHbsLRxW5v4pyddwgyTBK-9cKB1oD58 Change-Id: Ibe3a336ebb3525a719147fe6f991c910b214e370 --- data/keyboards/Vendor_18d1_Product_9451.kl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'data') diff --git a/data/keyboards/Vendor_18d1_Product_9451.kl b/data/keyboards/Vendor_18d1_Product_9451.kl index fb425be8bc23..2a1f897b5e08 100644 --- a/data/keyboards/Vendor_18d1_Product_9451.kl +++ b/data/keyboards/Vendor_18d1_Product_9451.kl @@ -17,15 +17,15 @@ # key 116 POWER WAKE -key 217 ASSIST WAKE +key 217 ASSIST key 103 DPAD_UP key 108 DPAD_DOWN key 105 DPAD_LEFT key 106 DPAD_RIGHT -key 353 DPAD_CENTER +key 353 DPAD_CENTER WAKE -key 158 BACK +key 158 BACK WAKE key 172 HOME WAKE key 113 VOLUME_MUTE -- cgit v1.2.3-59-g8ed1b From a582dde3b59440a20245b5e259f66d2c118dda57 Mon Sep 17 00:00:00 2001 From: dakinola Date: Wed, 25 Oct 2023 10:59:08 +0000 Subject: Report MediaProjectionTargetChanged Atom Update ContentRecorder & MediaProjectionManagerService to log a MediaProjectionTargetChanged atom upon recording starting and further updates to windowing mode. Bug: 304728422 Test: atest WmTests:ContentRecorderTests Test: atest FrameworksServicesTests:MediaProjectionManagerServiceTest Test: atest FrameworksServicesTests:MediaProjectionMetricsLoggerTest Change-Id: I5120ba2571fb2e6e084e72c4fd079767530ccdeb --- data/etc/services.core.protolog.json | 6 + .../media/projection/IMediaProjectionManager.aidl | 5 + .../media/projection/FrameworkStatsLogWrapper.java | 21 ++- .../projection/MediaProjectionManagerService.java | 26 +++ .../projection/MediaProjectionMetricsLogger.java | 114 +++++++++++- .../com/android/server/wm/ContentRecorder.java | 47 ++++- .../java/com/android/server/wm/DisplayContent.java | 3 +- .../MediaProjectionManagerServiceTest.java | 44 ++++- .../MediaProjectionMetricsLoggerTest.java | 203 ++++++++++++++++++--- .../android/server/wm/ContentRecorderTests.java | 71 ++++++- 10 files changed, 486 insertions(+), 54 deletions(-) (limited to 'data') diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 87be13ab4a93..dc2b0561957d 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -2653,6 +2653,12 @@ "group": "WM_DEBUG_WINDOW_TRANSITIONS", "at": "com\/android\/server\/wm\/TransitionController.java" }, + "261227010": { + "message": "Content Recording: Unable to tell log windowing mode change: %s", + "level": "ERROR", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecorder.java" + }, "269576220": { "message": "Resuming rotation after drag", "level": "DEBUG", diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl index 24efbd14bc02..a7ec6c692416 100644 --- a/media/java/android/media/projection/IMediaProjectionManager.aidl +++ b/media/java/android/media/projection/IMediaProjectionManager.aidl @@ -212,4 +212,9 @@ interface IMediaProjectionManager { @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") oneway void notifyAppSelectorDisplayed(int hostUid); + + @EnforcePermission("MANAGE_MEDIA_PROJECTION") + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + + ".permission.MANAGE_MEDIA_PROJECTION)") + void notifyWindowingModeChanged(int contentToRecord, int targetUid, int windowingMode); } diff --git a/services/core/java/com/android/server/media/projection/FrameworkStatsLogWrapper.java b/services/core/java/com/android/server/media/projection/FrameworkStatsLogWrapper.java index 5bad06777328..6c74cba99bcb 100644 --- a/services/core/java/com/android/server/media/projection/FrameworkStatsLogWrapper.java +++ b/services/core/java/com/android/server/media/projection/FrameworkStatsLogWrapper.java @@ -21,8 +21,8 @@ import com.android.internal.util.FrameworkStatsLog; /** Wrapper around {@link FrameworkStatsLog} */ public class FrameworkStatsLogWrapper { - /** Wrapper around {@link FrameworkStatsLog#write}. */ - public void write( + /** Wrapper around {@link FrameworkStatsLog#write} for MediaProjectionStateChanged atom. */ + public void writeStateChanged( int code, int sessionId, int state, @@ -41,4 +41,21 @@ public class FrameworkStatsLogWrapper { timeSinceLastActive, creationSource); } + + /** Wrapper around {@link FrameworkStatsLog#write} for MediaProjectionTargetChanged atom. */ + public void writeTargetChanged( + int code, + int sessionId, + int targetType, + int hostUid, + int targetUid, + int windowingMode) { + FrameworkStatsLog.write( + code, + sessionId, + targetType, + hostUid, + targetUid, + windowingMode); + } } diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index 893ed6119f9f..6deda468f9a2 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -479,6 +479,18 @@ public final class MediaProjectionManagerService extends SystemService mMediaProjectionMetricsLogger.logAppSelectorDisplayed(hostUid); } + @VisibleForTesting + void notifyWindowingModeChanged(int contentToRecord, int targetUid, int windowingMode) { + synchronized (mLock) { + if (mProjectionGrant == null) { + Slog.i(TAG, "Cannot log MediaProjectionTargetChanged atom due to null projection"); + } else { + mMediaProjectionMetricsLogger.logChangedWindowingMode( + contentToRecord, mProjectionGrant.uid, targetUid, windowingMode); + } + } + } + /** * Handles result of dialog shown from * {@link BinderService#buildReviewGrantedConsentIntentLocked()}. @@ -904,6 +916,20 @@ public final class MediaProjectionManagerService extends SystemService } } + @Override // Binder call + @EnforcePermission(MANAGE_MEDIA_PROJECTION) + public void notifyWindowingModeChanged( + int contentToRecord, int targetUid, int windowingMode) { + notifyWindowingModeChanged_enforcePermission(); + final long token = Binder.clearCallingIdentity(); + try { + MediaProjectionManagerService.this.notifyWindowingModeChanged( + contentToRecord, targetUid, windowingMode); + } finally { + Binder.restoreCallingIdentity(token); + } + } + @Override // Binder call public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionMetricsLogger.java b/services/core/java/com/android/server/media/projection/MediaProjectionMetricsLogger.java index d7fefeb0b1fe..be2a25a755a5 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionMetricsLogger.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionMetricsLogger.java @@ -16,16 +16,32 @@ package com.android.server.media.projection; +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.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY; +import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK; + import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__CREATION_SOURCE__CREATION_SOURCE_UNKNOWN; import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_APP_SELECTOR_DISPLAYED; import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_CAPTURING_IN_PROGRESS; import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_INITIATED; import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_PERMISSION_REQUEST_DISPLAYED; import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_STOPPED; +import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_TARGET_CHANGED__TARGET_TYPE__TARGET_TYPE_APP_TASK; +import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_TARGET_CHANGED__TARGET_TYPE__TARGET_TYPE_DISPLAY; +import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_TARGET_CHANGED__TARGET_TYPE__TARGET_TYPE_UNKNOWN; +import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_TARGET_CHANGED__TARGET_WINDOWING_MODE__WINDOWING_MODE_FREEFORM; +import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_TARGET_CHANGED__TARGET_WINDOWING_MODE__WINDOWING_MODE_FULLSCREEN; +import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_TARGET_CHANGED__TARGET_WINDOWING_MODE__WINDOWING_MODE_SPLIT_SCREEN; +import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_TARGET_CHANGED__TARGET_WINDOWING_MODE__WINDOWING_MODE_UNKNOWN; +import android.app.WindowConfiguration.WindowingMode; import android.content.Context; import android.util.Log; +import android.view.ContentRecordingSession.RecordContent; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.FrameworkStatsLog; import java.time.Duration; @@ -91,7 +107,7 @@ public class MediaProjectionMetricsLogger { durationSinceLastActiveSession == null ? TIME_SINCE_LAST_ACTIVE_UNKNOWN : (int) durationSinceLastActiveSession.toSeconds(); - write( + writeStateChanged( mSessionIdGenerator.createAndGetNewSessionId(), MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_INITIATED, hostUid, @@ -102,13 +118,13 @@ public class MediaProjectionMetricsLogger { /** * Logs that the user entered the setup flow and permission dialog is displayed. This state is - * not sent when the permission is already granted and we skipped showing the permission dialog. + * not sent when the permission is already granted, and we skipped showing the permission dialog. * * @param hostUid UID of the package that initiates MediaProjection. */ public void logPermissionRequestDisplayed(int hostUid) { Log.d(TAG, "logPermissionRequestDisplayed"); - write( + writeStateChanged( mSessionIdGenerator.getCurrentSessionId(), MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_PERMISSION_REQUEST_DISPLAYED, hostUid, @@ -123,7 +139,7 @@ public class MediaProjectionMetricsLogger { * @param hostUid UID of the package that initiates MediaProjection. */ public void logProjectionPermissionRequestCancelled(int hostUid) { - write( + writeStateChanged( mSessionIdGenerator.getCurrentSessionId(), FrameworkStatsLog .MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_CANCELLED, @@ -141,7 +157,7 @@ public class MediaProjectionMetricsLogger { */ public void logAppSelectorDisplayed(int hostUid) { Log.d(TAG, "logAppSelectorDisplayed"); - write( + writeStateChanged( mSessionIdGenerator.getCurrentSessionId(), MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_APP_SELECTOR_DISPLAYED, hostUid, @@ -158,7 +174,7 @@ public class MediaProjectionMetricsLogger { */ public void logInProgress(int hostUid, int targetUid) { Log.d(TAG, "logInProgress"); - write( + writeStateChanged( mSessionIdGenerator.getCurrentSessionId(), MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_CAPTURING_IN_PROGRESS, hostUid, @@ -167,6 +183,54 @@ public class MediaProjectionMetricsLogger { MEDIA_PROJECTION_STATE_CHANGED__CREATION_SOURCE__CREATION_SOURCE_UNKNOWN); } + /** + * Logs that the windowing mode of a projection has changed. + * + * @param contentToRecord ContentRecordingSession.RecordContent indicating whether it is a + * task capture or display capture - gets converted to the corresponding + * TargetType before being logged. + * @param hostUid UID of the package that initiates MediaProjection. + * @param targetUid UID of the package that is captured if selected. + * @param windowingMode Updated WindowConfiguration.WindowingMode of the captured region - gets + * converted to the corresponding TargetWindowingMode before being logged. + */ + public void logChangedWindowingMode( + int contentToRecord, int hostUid, int targetUid, int windowingMode) { + Log.d(TAG, "logChangedWindowingMode"); + writeTargetChanged( + mSessionIdGenerator.getCurrentSessionId(), + contentToRecordToTargetType(contentToRecord), + hostUid, + targetUid, + windowingModeToTargetWindowingMode(windowingMode)); + + } + + @VisibleForTesting + public int contentToRecordToTargetType(@RecordContent int recordContentType) { + return switch (recordContentType) { + case RECORD_CONTENT_DISPLAY -> + MEDIA_PROJECTION_TARGET_CHANGED__TARGET_TYPE__TARGET_TYPE_DISPLAY; + case RECORD_CONTENT_TASK -> + MEDIA_PROJECTION_TARGET_CHANGED__TARGET_TYPE__TARGET_TYPE_APP_TASK; + default -> MEDIA_PROJECTION_TARGET_CHANGED__TARGET_TYPE__TARGET_TYPE_UNKNOWN; + }; + } + + @VisibleForTesting + public int windowingModeToTargetWindowingMode(@WindowingMode int windowingMode) { + return switch (windowingMode) { + case WINDOWING_MODE_FULLSCREEN -> + MEDIA_PROJECTION_TARGET_CHANGED__TARGET_WINDOWING_MODE__WINDOWING_MODE_FULLSCREEN; + case WINDOWING_MODE_FREEFORM -> + MEDIA_PROJECTION_TARGET_CHANGED__TARGET_WINDOWING_MODE__WINDOWING_MODE_FREEFORM; + case WINDOWING_MODE_MULTI_WINDOW -> + MEDIA_PROJECTION_TARGET_CHANGED__TARGET_WINDOWING_MODE__WINDOWING_MODE_SPLIT_SCREEN; + default -> + MEDIA_PROJECTION_TARGET_CHANGED__TARGET_WINDOWING_MODE__WINDOWING_MODE_UNKNOWN; + }; + } + /** * Logs that the capturing stopped, either normally or because of error. * @@ -178,7 +242,7 @@ public class MediaProjectionMetricsLogger { mPreviousState == MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_CAPTURING_IN_PROGRESS; Log.d(TAG, "logStopped: wasCaptureInProgress=" + wasCaptureInProgress); - write( + writeStateChanged( mSessionIdGenerator.getCurrentSessionId(), MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_STOPPED, hostUid, @@ -191,14 +255,31 @@ public class MediaProjectionMetricsLogger { } } - private void write( + public void notifyProjectionStateChange(int hostUid, int state, int sessionCreationSource) { + writeStateChanged(hostUid, state, sessionCreationSource); + } + + private void writeStateChanged(int hostUid, int state, int sessionCreationSource) { + mFrameworkStatsLogWrapper.writeStateChanged( + /* code */ FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED, + /* session_id */ 123, + /* state */ state, + /* previous_state */ FrameworkStatsLog + .MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_UNKNOWN, + /* host_uid */ hostUid, + /* target_uid */ -1, + /* time_since_last_active */ 0, + /* creation_source */ sessionCreationSource); + } + + private void writeStateChanged( int sessionId, int state, int hostUid, int targetUid, int timeSinceLastActive, int creationSource) { - mFrameworkStatsLogWrapper.write( + mFrameworkStatsLogWrapper.writeStateChanged( /* code */ FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED, sessionId, state, @@ -209,4 +290,19 @@ public class MediaProjectionMetricsLogger { creationSource); mPreviousState = state; } + + private void writeTargetChanged( + int sessionId, + int targetType, + int hostUid, + int targetUid, + int targetWindowingMode) { + mFrameworkStatsLogWrapper.writeTargetChanged( + /* code */ FrameworkStatsLog.MEDIA_PROJECTION_TARGET_CHANGED, + sessionId, + targetType, + hostUid, + targetUid, + targetWindowingMode); + } } diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java index 022ef6152929..8717098ff8e8 100644 --- a/services/core/java/com/android/server/wm/ContentRecorder.java +++ b/services/core/java/com/android/server/wm/ContentRecorder.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.content.Context.MEDIA_PROJECTION_SERVICE; import static android.content.res.Configuration.ORIENTATION_UNDEFINED; import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY; @@ -100,6 +101,8 @@ final class ContentRecorder implements WindowContainerListener { @Configuration.Orientation private int mLastOrientation = ORIENTATION_UNDEFINED; + private int mLastWindowingMode = WINDOWING_MODE_UNDEFINED; + private final boolean mCorrectForAnisotropicPixels; ContentRecorder(@NonNull DisplayContent displayContent) { @@ -156,7 +159,8 @@ final class ContentRecorder implements WindowContainerListener { * Handle a configuration change on the display content, and resize recording if needed. * @param lastOrientation the prior orientation of the configuration */ - void onConfigurationChanged(@Configuration.Orientation int lastOrientation) { + void onConfigurationChanged( + @Configuration.Orientation int lastOrientation, int lastWindowingMode) { // Update surface for MediaProjection, if this DisplayContent is being used for recording. if (!isCurrentlyRecording() || mLastRecordedBounds == null) { return; @@ -185,6 +189,16 @@ final class ContentRecorder implements WindowContainerListener { } } + // Record updated windowing mode, if necessary. + int recordedContentWindowingMode = mRecordedWindowContainer.getWindowingMode(); + if (lastWindowingMode != recordedContentWindowingMode) { + mMediaProjectionManager.notifyWindowingModeChanged( + mContentRecordingSession.getContentToRecord(), + mContentRecordingSession.getTargetUid(), + recordedContentWindowingMode + ); + } + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, "Content Recording: Display %d was already recording, so apply " + "transformations if necessary", @@ -327,8 +341,10 @@ final class ContentRecorder implements WindowContainerListener { return; } + final int contentToRecord = mContentRecordingSession.getContentToRecord(); + // TODO(b/297514518) Do not start capture if the app is in PIP, the bounds are inaccurate. - if (mContentRecordingSession.getContentToRecord() == RECORD_CONTENT_TASK) { + if (contentToRecord == RECORD_CONTENT_TASK) { if (mRecordedWindowContainer.asTask().inPinnedWindowingMode()) { ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, "Content Recording: Display %d should start recording, but " @@ -375,7 +391,7 @@ final class ContentRecorder implements WindowContainerListener { // Notify the client about the visibility of the mirrored region, now that we have begun // capture. - if (mContentRecordingSession.getContentToRecord() == RECORD_CONTENT_TASK) { + if (contentToRecord == RECORD_CONTENT_TASK) { mMediaProjectionManager.notifyActiveProjectionCapturedContentVisibilityChanged( mRecordedWindowContainer.asTask().isVisibleRequested()); } else { @@ -385,6 +401,11 @@ final class ContentRecorder implements WindowContainerListener { currentDisplayState != DISPLAY_STATE_OFF); } + // Record initial windowing mode after recording starts. + mMediaProjectionManager.notifyWindowingModeChanged( + contentToRecord, mContentRecordingSession.getTargetUid(), + mRecordedWindowContainer.getWindowConfiguration().getWindowingMode()); + // No need to clean up. In SurfaceFlinger, parents hold references to their children. The // mirrored SurfaceControl is alive since the parent DisplayContent SurfaceControl is // holding a reference to it. Therefore, the mirrored SurfaceControl will be cleaned up @@ -617,8 +638,9 @@ final class ContentRecorder implements WindowContainerListener { Configuration mergedOverrideConfiguration) { WindowContainerListener.super.onMergedOverrideConfigurationChanged( mergedOverrideConfiguration); - onConfigurationChanged(mLastOrientation); + onConfigurationChanged(mLastOrientation, mLastWindowingMode); mLastOrientation = mergedOverrideConfiguration.orientation; + mLastWindowingMode = mergedOverrideConfiguration.windowConfiguration.getWindowingMode(); } // WindowContainerListener @@ -635,6 +657,7 @@ final class ContentRecorder implements WindowContainerListener { void stopActiveProjection(); void notifyActiveProjectionCapturedContentResized(int width, int height); void notifyActiveProjectionCapturedContentVisibilityChanged(boolean isVisible); + void notifyWindowingModeChanged(int contentToRecord, int targetUid, int windowingMode); } private static final class RemoteMediaProjectionManagerWrapper implements @@ -700,6 +723,22 @@ final class ContentRecorder implements WindowContainerListener { } } + @Override + public void notifyWindowingModeChanged(int contentToRecord, int targetUid, + int windowingMode) { + fetchMediaProjectionManager(); + if (mIMediaProjectionManager == null) { + return; + } + try { + mIMediaProjectionManager.notifyWindowingModeChanged( + contentToRecord, targetUid, windowingMode); + } catch (RemoteException e) { + ProtoLog.e(WM_DEBUG_CONTENT_RECORDING, + "Content Recording: Unable to tell log windowing mode change: %s", e); + } + } + private void fetchMediaProjectionManager() { if (mIMediaProjectionManager != null) { return; diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 576e8a48ab31..c716879a5097 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -2757,6 +2757,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp @Override public void onConfigurationChanged(Configuration newParentConfig) { final int lastOrientation = getConfiguration().orientation; + final int lastWindowingMode = getWindowingMode(); super.onConfigurationChanged(newParentConfig); if (mDisplayPolicy != null) { mDisplayPolicy.onConfigurationChanged(); @@ -2768,7 +2769,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // Update surface for MediaProjection, if this DisplayContent is being used for recording. if (mContentRecorder != null) { - mContentRecorder.onConfigurationChanged(lastOrientation); + mContentRecorder.onConfigurationChanged(lastOrientation, lastWindowingMode); } if (lastOrientation != getConfiguration().orientation) { diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java index ece3dfeabafa..097cc5177a83 100644 --- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java @@ -17,12 +17,17 @@ package com.android.server.media.projection; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_PRIVILEGED; import static android.media.projection.MediaProjectionManager.TYPE_MIRRORING; import static android.media.projection.ReviewGrantedConsentResult.RECORD_CANCEL; import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_DISPLAY; import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_TASK; import static android.media.projection.ReviewGrantedConsentResult.UNKNOWN; +import static android.view.ContentRecordingSession.TARGET_UID_FULL_SCREEN; +import static android.view.ContentRecordingSession.TARGET_UID_UNKNOWN; +import static android.view.ContentRecordingSession.createDisplaySession; +import static android.view.ContentRecordingSession.createTaskSession; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; @@ -62,6 +67,7 @@ import android.os.UserHandle; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; import android.view.ContentRecordingSession; +import android.view.ContentRecordingSession.RecordContent; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.FlakyTest; @@ -99,7 +105,7 @@ public class MediaProjectionManagerServiceTest { private final ApplicationInfo mAppInfo = new ApplicationInfo(); private final TestLooper mTestLooper = new TestLooper(); private static final ContentRecordingSession DISPLAY_SESSION = - ContentRecordingSession.createDisplaySession(DEFAULT_DISPLAY); + createDisplaySession(DEFAULT_DISPLAY); // Callback registered by an app on a MediaProjection instance. private final FakeIMediaProjectionCallback mIMediaProjectionCallback = new FakeIMediaProjectionCallback(); @@ -142,7 +148,7 @@ public class MediaProjectionManagerServiceTest { private MediaProjectionManagerService mService; private OffsettableClock mClock; private ContentRecordingSession mWaitingDisplaySession = - ContentRecordingSession.createDisplaySession(DEFAULT_DISPLAY); + createDisplaySession(DEFAULT_DISPLAY); @Mock private ActivityManagerInternal mAmInternal; @@ -333,7 +339,7 @@ public class MediaProjectionManagerServiceTest { projection.stop(); verify(mMediaProjectionMetricsLogger) - .logStopped(UID, ContentRecordingSession.TARGET_UID_UNKNOWN); + .logStopped(UID, TARGET_UID_UNKNOWN); } @Test @@ -351,7 +357,7 @@ public class MediaProjectionManagerServiceTest { projection.stop(); verify(mMediaProjectionMetricsLogger) - .logStopped(UID, ContentRecordingSession.TARGET_UID_FULL_SCREEN); + .logStopped(UID, TARGET_UID_FULL_SCREEN); } @Test @@ -366,7 +372,7 @@ public class MediaProjectionManagerServiceTest { .when(mWindowManagerInternal) .setContentRecordingSession(any(ContentRecordingSession.class)); ContentRecordingSession taskSession = - ContentRecordingSession.createTaskSession(mock(IBinder.class), targetUid); + createTaskSession(mock(IBinder.class), targetUid); service.setContentRecordingSession(taskSession); projection.stop(); @@ -695,6 +701,26 @@ public class MediaProjectionManagerServiceTest { verify(mMediaProjectionMetricsLogger).logAppSelectorDisplayed(hostUid); } + @Test + public void notifyWindowingModeChanged_forwardsToLogger() throws Exception { + int targetUid = 123; + mService = + new MediaProjectionManagerService(mContext, mMediaProjectionMetricsLoggerInjector); + + ContentRecordingSession taskSession = + createTaskSession(mock(IBinder.class), targetUid); + mService.setContentRecordingSession(taskSession); + + MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(); + projection.start(mIMediaProjectionCallback); + + mService.notifyWindowingModeChanged( + RECORD_CONTENT_TASK, targetUid, WINDOWING_MODE_MULTI_WINDOW); + + verify(mMediaProjectionMetricsLogger).logChangedWindowingMode(RECORD_CONTENT_TASK, + projection.uid, targetUid, WINDOWING_MODE_MULTI_WINDOW); + } + /** * Executes and validates scenario where the consent result indicates the projection ends. */ @@ -755,7 +781,7 @@ public class MediaProjectionManagerServiceTest { */ private void testSetUserReviewGrantedConsentResult_startedSession( @ReviewGrantedConsentResult int consentResult, - @ContentRecordingSession.RecordContent int recordedContent) + @RecordContent int recordedContent) throws NameNotFoundException { MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(); projection.setLaunchCookie(mock(IBinder.class)); @@ -777,7 +803,7 @@ public class MediaProjectionManagerServiceTest { */ private void testSetUserReviewGrantedConsentResult_failedToStartSession( @ReviewGrantedConsentResult int consentResult, - @ContentRecordingSession.RecordContent int recordedContent) + @RecordContent int recordedContent) throws NameNotFoundException { MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(); projection.start(mIMediaProjectionCallback); @@ -889,7 +915,7 @@ public class MediaProjectionManagerServiceTest { int targetUid = 123455; ContentRecordingSession taskSession = - ContentRecordingSession.createTaskSession(mock(IBinder.class), targetUid); + createTaskSession(mock(IBinder.class), targetUid); service.setContentRecordingSession(taskSession); verify(mMediaProjectionMetricsLogger).logInProgress(projection.uid, targetUid); @@ -970,7 +996,7 @@ public class MediaProjectionManagerServiceTest { verify(mWatcherCallback, never()).onRecordingSessionSet(any(), any()); } - private void verifySetSessionWithContent(@ContentRecordingSession.RecordContent int content) { + private void verifySetSessionWithContent(@RecordContent int content) { verify(mWindowManagerInternal, atLeastOnce()).setContentRecordingSession( mSessionCaptor.capture()); assertThat(mSessionCaptor.getValue()).isNotNull(); diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionMetricsLoggerTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionMetricsLoggerTest.java index ad1cd6eca5ac..72ce9fe9a23f 100644 --- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionMetricsLoggerTest.java +++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionMetricsLoggerTest.java @@ -16,6 +16,14 @@ package com.android.server.media.projection; +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 android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY; +import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK; + import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__CREATION_SOURCE__CREATION_SOURCE_UNKNOWN; import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_APP_SELECTOR_DISPLAYED; import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_CANCELLED; @@ -24,6 +32,13 @@ import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_PERMISSION_REQUEST_DISPLAYED; import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_STOPPED; import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_UNKNOWN; +import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_TARGET_CHANGED__TARGET_TYPE__TARGET_TYPE_APP_TASK; +import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_TARGET_CHANGED__TARGET_TYPE__TARGET_TYPE_DISPLAY; +import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_TARGET_CHANGED__TARGET_TYPE__TARGET_TYPE_UNKNOWN; +import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_TARGET_CHANGED__TARGET_WINDOWING_MODE__WINDOWING_MODE_FREEFORM; +import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_TARGET_CHANGED__TARGET_WINDOWING_MODE__WINDOWING_MODE_FULLSCREEN; +import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_TARGET_CHANGED__TARGET_WINDOWING_MODE__WINDOWING_MODE_SPLIT_SCREEN; +import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_TARGET_CHANGED__TARGET_WINDOWING_MODE__WINDOWING_MODE_UNKNOWN; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -38,10 +53,14 @@ import androidx.test.filters.SmallTest; import com.android.internal.util.FrameworkStatsLog; +import com.google.common.truth.Expect; + import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.time.Duration; @@ -60,12 +79,18 @@ public class MediaProjectionMetricsLoggerTest { private static final int TEST_TARGET_UID = 456; private static final int TEST_CREATION_SOURCE = 789; + private static final int TEST_WINDOWING_MODE = 987; + private static final int TEST_CONTENT_TO_RECORD = 654; + @Mock private FrameworkStatsLogWrapper mFrameworkStatsLogWrapper; @Mock private MediaProjectionSessionIdGenerator mSessionIdGenerator; @Mock private MediaProjectionTimestampStore mTimestampStore; private MediaProjectionMetricsLogger mLogger; + @Rule + public Expect mExpect = Expect.create(); + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -93,7 +118,7 @@ public class MediaProjectionMetricsLoggerTest { public void logInitiated_logsHostUid() { mLogger.logInitiated(TEST_HOST_UID, TEST_CREATION_SOURCE); - verifyHostUidLogged(TEST_HOST_UID); + verifyStateChangedHostUidLogged(TEST_HOST_UID); } @Test @@ -107,7 +132,7 @@ public class MediaProjectionMetricsLoggerTest { public void logInitiated_logsUnknownTargetUid() { mLogger.logInitiated(TEST_HOST_UID, TEST_CREATION_SOURCE); - verifyTargetUidLogged(-2); + verifyStageChangedTargetUidLogged(-2); } @Test @@ -178,14 +203,14 @@ public class MediaProjectionMetricsLoggerTest { public void logStopped_logsHostUid() { mLogger.logStopped(TEST_HOST_UID, TEST_TARGET_UID); - verifyHostUidLogged(TEST_HOST_UID); + verifyStateChangedHostUidLogged(TEST_HOST_UID); } @Test public void logStopped_logsTargetUid() { mLogger.logStopped(TEST_HOST_UID, TEST_TARGET_UID); - verifyTargetUidLogged(TEST_TARGET_UID); + verifyStageChangedTargetUidLogged(TEST_TARGET_UID); } @Test @@ -263,14 +288,14 @@ public class MediaProjectionMetricsLoggerTest { public void logInProgress_logsHostUid() { mLogger.logInProgress(TEST_HOST_UID, TEST_TARGET_UID); - verifyHostUidLogged(TEST_HOST_UID); + verifyStateChangedHostUidLogged(TEST_HOST_UID); } @Test public void logInProgress_logsTargetUid() { mLogger.logInProgress(TEST_HOST_UID, TEST_TARGET_UID); - verifyTargetUidLogged(TEST_TARGET_UID); + verifyStageChangedTargetUidLogged(TEST_TARGET_UID); } @Test @@ -336,14 +361,14 @@ public class MediaProjectionMetricsLoggerTest { public void logPermissionRequestDisplayed_logsHostUid() { mLogger.logPermissionRequestDisplayed(TEST_HOST_UID); - verifyHostUidLogged(TEST_HOST_UID); + verifyStateChangedHostUidLogged(TEST_HOST_UID); } @Test public void logPermissionRequestDisplayed_logsUnknownTargetUid() { mLogger.logPermissionRequestDisplayed(TEST_HOST_UID); - verifyTargetUidLogged(-2); + verifyStageChangedTargetUidLogged(-2); } @Test @@ -409,14 +434,14 @@ public class MediaProjectionMetricsLoggerTest { public void logAppSelectorDisplayed_logsHostUid() { mLogger.logAppSelectorDisplayed(TEST_HOST_UID); - verifyHostUidLogged(TEST_HOST_UID); + verifyStateChangedHostUidLogged(TEST_HOST_UID); } @Test public void logAppSelectorDisplayed_logsUnknownTargetUid() { mLogger.logAppSelectorDisplayed(TEST_HOST_UID); - verifyTargetUidLogged(-2); + verifyStageChangedTargetUidLogged(-2); } @Test @@ -492,14 +517,14 @@ public class MediaProjectionMetricsLoggerTest { public void logProjectionPermissionRequestCancelled_logsHostUid() { mLogger.logProjectionPermissionRequestCancelled(TEST_HOST_UID); - verifyHostUidLogged(TEST_HOST_UID); + verifyStateChangedHostUidLogged(TEST_HOST_UID); } @Test public void logProjectionPermissionRequestCancelled_logsUnknownTargetUid() { mLogger.logProjectionPermissionRequestCancelled(TEST_HOST_UID); - verifyTargetUidLogged(-2); + verifyStageChangedTargetUidLogged(-2); } @Test @@ -510,9 +535,88 @@ public class MediaProjectionMetricsLoggerTest { MEDIA_PROJECTION_STATE_CHANGED__CREATION_SOURCE__CREATION_SOURCE_UNKNOWN); } + @Test + public void logWindowingModeChanged_logsTargetChangedAtomId() { + mLogger.logChangedWindowingMode( + TEST_CONTENT_TO_RECORD, TEST_HOST_UID, TEST_TARGET_UID, TEST_WINDOWING_MODE); + + verifyTargetChangedAtomIdLogged(); + } + + @Test + public void logWindowingModeChanged_logsTargetType() { + MediaProjectionMetricsLogger logger = Mockito.spy(mLogger); + final int testTargetType = 111; + when(logger.contentToRecordToTargetType(TEST_CONTENT_TO_RECORD)).thenReturn(testTargetType); + logger.logChangedWindowingMode( + TEST_CONTENT_TO_RECORD, TEST_HOST_UID, TEST_TARGET_UID, TEST_WINDOWING_MODE); + verifyTargetTypeLogged(testTargetType); + } + + @Test + public void logWindowingModeChanged_logsHostUid() { + mLogger.logChangedWindowingMode( + TEST_CONTENT_TO_RECORD, TEST_HOST_UID, TEST_TARGET_UID, TEST_WINDOWING_MODE); + verifyTargetChangedHostUidLogged(TEST_HOST_UID); + } + + @Test + public void logWindowingModeChanged_logsTargetUid() { + mLogger.logChangedWindowingMode( + TEST_CONTENT_TO_RECORD, TEST_HOST_UID, TEST_TARGET_UID, TEST_WINDOWING_MODE); + verifyTargetChangedTargetUidLogged(TEST_TARGET_UID); + } + + @Test + public void logWindowingModeChanged_logsTargetWindowingMode() { + MediaProjectionMetricsLogger logger = Mockito.spy(mLogger); + final int testTargetWindowingMode = 222; + when(logger.windowingModeToTargetWindowingMode(TEST_WINDOWING_MODE)) + .thenReturn(testTargetWindowingMode); + logger.logChangedWindowingMode( + TEST_CONTENT_TO_RECORD, TEST_HOST_UID, TEST_TARGET_UID, TEST_WINDOWING_MODE); + verifyWindowingModeLogged(testTargetWindowingMode); + } + + @Test + public void testContentToRecordToTargetType() { + mExpect.that(mLogger.contentToRecordToTargetType(RECORD_CONTENT_DISPLAY)) + .isEqualTo(MEDIA_PROJECTION_TARGET_CHANGED__TARGET_TYPE__TARGET_TYPE_DISPLAY); + + mExpect.that(mLogger.contentToRecordToTargetType(RECORD_CONTENT_TASK)) + .isEqualTo(MEDIA_PROJECTION_TARGET_CHANGED__TARGET_TYPE__TARGET_TYPE_APP_TASK); + + mExpect.that(mLogger.contentToRecordToTargetType(2)) + .isEqualTo(MEDIA_PROJECTION_TARGET_CHANGED__TARGET_TYPE__TARGET_TYPE_UNKNOWN); + + mExpect.that(mLogger.contentToRecordToTargetType(-1)) + .isEqualTo(MEDIA_PROJECTION_TARGET_CHANGED__TARGET_TYPE__TARGET_TYPE_UNKNOWN); + + mExpect.that(mLogger.contentToRecordToTargetType(100)) + .isEqualTo(MEDIA_PROJECTION_TARGET_CHANGED__TARGET_TYPE__TARGET_TYPE_UNKNOWN); + } + + @Test + public void testWindowingModeToTargetWindowingMode() { + mExpect.that(mLogger.windowingModeToTargetWindowingMode(WINDOWING_MODE_FULLSCREEN)) + .isEqualTo(MEDIA_PROJECTION_TARGET_CHANGED__TARGET_WINDOWING_MODE__WINDOWING_MODE_FULLSCREEN); + + mExpect.that(mLogger.windowingModeToTargetWindowingMode(WINDOWING_MODE_MULTI_WINDOW)) + .isEqualTo(MEDIA_PROJECTION_TARGET_CHANGED__TARGET_WINDOWING_MODE__WINDOWING_MODE_SPLIT_SCREEN); + + mExpect.that(mLogger.windowingModeToTargetWindowingMode(WINDOWING_MODE_FREEFORM)) + .isEqualTo(MEDIA_PROJECTION_TARGET_CHANGED__TARGET_WINDOWING_MODE__WINDOWING_MODE_FREEFORM); + + mExpect.that(mLogger.windowingModeToTargetWindowingMode(WINDOWING_MODE_PINNED)) + .isEqualTo(MEDIA_PROJECTION_TARGET_CHANGED__TARGET_WINDOWING_MODE__WINDOWING_MODE_UNKNOWN); + + mExpect.that(mLogger.windowingModeToTargetWindowingMode(WINDOWING_MODE_UNDEFINED)) + .isEqualTo(MEDIA_PROJECTION_TARGET_CHANGED__TARGET_WINDOWING_MODE__WINDOWING_MODE_UNKNOWN); + } + private void verifyStateChangedAtomIdLogged() { verify(mFrameworkStatsLogWrapper) - .write( + .writeStateChanged( /* code= */ eq(FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED), /* sessionId= */ anyInt(), /* state= */ anyInt(), @@ -525,7 +629,7 @@ public class MediaProjectionMetricsLoggerTest { private void verifyStateLogged(int state) { verify(mFrameworkStatsLogWrapper) - .write( + .writeStateChanged( /* code= */ anyInt(), /* sessionId= */ anyInt(), eq(state), @@ -536,9 +640,9 @@ public class MediaProjectionMetricsLoggerTest { /* creationSource= */ anyInt()); } - private void verifyHostUidLogged(int hostUid) { + private void verifyStateChangedHostUidLogged(int hostUid) { verify(mFrameworkStatsLogWrapper) - .write( + .writeStateChanged( /* code= */ anyInt(), /* sessionId= */ anyInt(), /* state= */ anyInt(), @@ -551,7 +655,7 @@ public class MediaProjectionMetricsLoggerTest { private void verifyCreationSourceLogged(int creationSource) { verify(mFrameworkStatsLogWrapper) - .write( + .writeStateChanged( /* code= */ anyInt(), /* sessionId= */ anyInt(), /* state= */ anyInt(), @@ -562,9 +666,9 @@ public class MediaProjectionMetricsLoggerTest { eq(creationSource)); } - private void verifyTargetUidLogged(int targetUid) { + private void verifyStageChangedTargetUidLogged(int targetUid) { verify(mFrameworkStatsLogWrapper) - .write( + .writeStateChanged( /* code= */ anyInt(), /* sessionId= */ anyInt(), /* state= */ anyInt(), @@ -577,7 +681,7 @@ public class MediaProjectionMetricsLoggerTest { private void verifyTimeSinceLastActiveSessionLogged(int timeSinceLastActiveSession) { verify(mFrameworkStatsLogWrapper) - .write( + .writeStateChanged( /* code= */ anyInt(), /* sessionId= */ anyInt(), /* state= */ anyInt(), @@ -590,7 +694,7 @@ public class MediaProjectionMetricsLoggerTest { private void verifySessionIdLogged(int newSessionId) { verify(mFrameworkStatsLogWrapper) - .write( + .writeStateChanged( /* code= */ anyInt(), /* sessionId= */ eq(newSessionId), /* state= */ anyInt(), @@ -603,7 +707,7 @@ public class MediaProjectionMetricsLoggerTest { private void verifyPreviousStateLogged(int previousState) { verify(mFrameworkStatsLogWrapper) - .write( + .writeStateChanged( /* code= */ anyInt(), /* sessionId= */ anyInt(), /* state= */ anyInt(), @@ -613,4 +717,59 @@ public class MediaProjectionMetricsLoggerTest { /* timeSinceLastActive= */ anyInt(), /* creationSource= */ anyInt()); } + + private void verifyTargetChangedAtomIdLogged() { + verify(mFrameworkStatsLogWrapper) + .writeTargetChanged( + eq(FrameworkStatsLog.MEDIA_PROJECTION_TARGET_CHANGED), + /* sessionId= */ anyInt(), + /* targetType= */ anyInt(), + /* hostUid= */ anyInt(), + /* targetUid= */ anyInt(), + /* targetWindowingMode= */ anyInt()); + } + + private void verifyTargetTypeLogged(int targetType) { + verify(mFrameworkStatsLogWrapper) + .writeTargetChanged( + /* code= */ anyInt(), + /* sessionId= */ anyInt(), + eq(targetType), + /* hostUid= */ anyInt(), + /* targetUid= */ anyInt(), + /* targetWindowingMode= */ anyInt()); + } + + private void verifyTargetChangedHostUidLogged(int hostUid) { + verify(mFrameworkStatsLogWrapper) + .writeTargetChanged( + /* code= */ anyInt(), + /* sessionId= */ anyInt(), + /* targetType= */ anyInt(), + eq(hostUid), + /* targetUid= */ anyInt(), + /* targetWindowingMode= */ anyInt()); + } + + private void verifyTargetChangedTargetUidLogged(int targetUid) { + verify(mFrameworkStatsLogWrapper) + .writeTargetChanged( + /* code= */ anyInt(), + /* sessionId= */ anyInt(), + /* targetType= */ anyInt(), + /* hostUid= */ anyInt(), + eq(targetUid), + /* targetWindowingMode= */ anyInt()); + } + + private void verifyWindowingModeLogged(int targetWindowingMode) { + verify(mFrameworkStatsLogWrapper) + .writeTargetChanged( + /* code= */ anyInt(), + /* sessionId= */ anyInt(), + /* targetType= */ anyInt(), + /* hostUid= */ anyInt(), + /* targetUid= */ anyInt(), + eq(targetWindowingMode)); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java index 78566fb06179..887e5ee0c58a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java @@ -42,6 +42,7 @@ import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import android.app.WindowConfiguration; import android.content.pm.ActivityInfo; @@ -232,7 +233,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Test public void testOnConfigurationChanged_neverRecording() { defaultInit(); - mContentRecorder.onConfigurationChanged(ORIENTATION_PORTRAIT); + mContentRecorder.onConfigurationChanged(ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN); verify(mTransaction, never()).setPosition(eq(mRecordedSurface), anyFloat(), anyFloat()); verify(mTransaction, never()).setMatrix(eq(mRecordedSurface), anyFloat(), anyFloat(), @@ -248,7 +249,7 @@ public class ContentRecorderTests extends WindowTestsBase { @Configuration.Orientation final int lastOrientation = mDisplayContent.getConfiguration().orientation == ORIENTATION_PORTRAIT ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT; - mContentRecorder.onConfigurationChanged(lastOrientation); + mContentRecorder.onConfigurationChanged(lastOrientation, WINDOWING_MODE_FULLSCREEN); verify(mTransaction, atLeast(2)).setPosition(eq(mRecordedSurface), anyFloat(), anyFloat()); @@ -266,7 +267,8 @@ public class ContentRecorderTests extends WindowTestsBase { // The user rotates the device, so the host app resizes the virtual display for the capture. resizeDisplay(mDisplayContent, newWidth, mSurfaceSize.y); resizeDisplay(mVirtualDisplayContent, newWidth, mSurfaceSize.y); - mContentRecorder.onConfigurationChanged(mDisplayContent.getConfiguration().orientation); + mContentRecorder.onConfigurationChanged( + mDisplayContent.getConfiguration().orientation, WINDOWING_MODE_FULLSCREEN); verify(mTransaction, atLeast(2)).setPosition(eq(mRecordedSurface), anyFloat(), anyFloat()); @@ -283,7 +285,7 @@ public class ContentRecorderTests extends WindowTestsBase { // Change a value that we shouldn't rely upon; it has the wrong type. mVirtualDisplayContent.setOverrideOrientation(SCREEN_ORIENTATION_FULL_SENSOR); mContentRecorder.onConfigurationChanged( - mVirtualDisplayContent.getConfiguration().orientation); + mVirtualDisplayContent.getConfiguration().orientation, WINDOWING_MODE_FULLSCREEN); // No resize is issued, only the initial transformations when we started recording. verify(mTransaction).setPosition(eq(mRecordedSurface), anyFloat(), @@ -307,7 +309,7 @@ public class ContentRecorderTests extends WindowTestsBase { doReturn(newSurfaceSize).when(mWm.mDisplayManagerInternal).getDisplaySurfaceDefaultSize( anyInt()); mContentRecorder.onConfigurationChanged( - mVirtualDisplayContent.getConfiguration().orientation); + mVirtualDisplayContent.getConfiguration().orientation, WINDOWING_MODE_FULLSCREEN); // No resize is issued, only the initial transformations when we started recording. verify(mTransaction, atLeast(2)).setPosition(eq(mRecordedSurface), anyFloat(), @@ -378,6 +380,55 @@ public class ContentRecorderTests extends WindowTestsBase { recordedWidth, recordedHeight); } + @Test + public void testTaskWindowingModeChanged_changeWindowMode_notifyWindowModeChanged() { + defaultInit(); + // WHEN a recording is ongoing. + mTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + mContentRecorder.setContentRecordingSession(mTaskSession); + mContentRecorder.updateRecording(); + assertThat(mContentRecorder.isCurrentlyRecording()).isTrue(); + + // THEN the windowing mode change callback is notified. + verify(mMediaProjectionManagerWrapper) + .notifyWindowingModeChanged(mTaskSession.getContentToRecord(), + mTaskSession.getTargetUid(), WINDOWING_MODE_FULLSCREEN); + + // WHEN a configuration change arrives, and the task is now multi-window mode. + mTask.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + Configuration configuration = mTask.getConfiguration(); + mTask.onConfigurationChanged(configuration); + + // THEN windowing mode change callback is notified again. + verify(mMediaProjectionManagerWrapper) + .notifyWindowingModeChanged(mTaskSession.getContentToRecord(), + mTaskSession.getTargetUid(), WINDOWING_MODE_MULTI_WINDOW); + } + + @Test + public void testTaskWindowingModeChanged_sameWindowMode_notifyWindowModeChanged() { + defaultInit(); + // WHEN a recording is ongoing. + mTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + mContentRecorder.setContentRecordingSession(mTaskSession); + mContentRecorder.updateRecording(); + assertThat(mContentRecorder.isCurrentlyRecording()).isTrue(); + + // THEN the windowing mode change callback is notified. + verify(mMediaProjectionManagerWrapper) + .notifyWindowingModeChanged(mTaskSession.getContentToRecord(), + mTaskSession.getTargetUid(), WINDOWING_MODE_FULLSCREEN); + + // WHEN a configuration change arrives, and the task is STILL fullscreen. + mTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + Configuration configuration = mTask.getConfiguration(); + mTask.onConfigurationChanged(configuration); + + // THEN the windowing mode change callback is NOT called notified again. + verify(mMediaProjectionManagerWrapper, times(1)) + .notifyWindowingModeChanged(anyInt(), anyInt(), anyInt()); + } + @Test public void testTaskWindowingModeChanged_pip_stopsRecording() { defaultInit(); @@ -421,9 +472,12 @@ public class ContentRecorderTests extends WindowTestsBase { mContentRecorder.updateRecording(); assertThat(mContentRecorder.isCurrentlyRecording()).isTrue(); - // THEN the visibility change callback is notified. + // THEN the visibility change & windowing mode change callbacks are notified. verify(mMediaProjectionManagerWrapper) .notifyActiveProjectionCapturedContentVisibilityChanged(true); + verify(mMediaProjectionManagerWrapper) + .notifyWindowingModeChanged(mTaskSession.getContentToRecord(), + mTaskSession.getTargetUid(), mRootWindowContainer.getWindowingMode()); } @Test @@ -434,9 +488,12 @@ public class ContentRecorderTests extends WindowTestsBase { mContentRecorder.updateRecording(); assertThat(mContentRecorder.isCurrentlyRecording()).isTrue(); - // THEN the visibility change callback is notified. + // THEN the visibility change & windowing mode change callbacks are notified. verify(mMediaProjectionManagerWrapper) .notifyActiveProjectionCapturedContentVisibilityChanged(true); + verify(mMediaProjectionManagerWrapper) + .notifyWindowingModeChanged(mDisplaySession.getContentToRecord(), + mDisplaySession.getTargetUid(), mRootWindowContainer.getWindowingMode()); } @Test -- cgit v1.2.3-59-g8ed1b From 0162f2a1cf739b6f38ccebde4bbfec1c5c86d17d Mon Sep 17 00:00:00 2001 From: Makoto Onuki Date: Tue, 31 Oct 2023 09:13:53 -0700 Subject: New Importance API for Rb Bug: 292533010 Test: atest CtsGetBindingUidImportanceTest ... with and without android.app.get_binding_uid_importance set. Change-Id: Iee6f0e08ba499f2f51d8173e45168c69933cd451 --- core/api/system-current.txt | 2 + core/java/android/app/ActivityManager.java | 28 ++++++- core/java/android/app/IActivityManager.aidl | 1 + core/java/android/app/activity_manager.aconfig | 9 ++- core/res/AndroidManifest.xml | 9 +++ data/etc/privapp-permissions-platform.xml | 1 + packages/Shell/AndroidManifest.xml | 1 + .../android/server/am/ActivityManagerService.java | 93 +++++++++++++++++++++- 8 files changed, 139 insertions(+), 5 deletions(-) (limited to 'data') diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 0ad73af28a2c..0cfd10f63f38 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -133,6 +133,7 @@ package android { field public static final String FORCE_STOP_PACKAGES = "android.permission.FORCE_STOP_PACKAGES"; field public static final String GET_APP_METADATA = "android.permission.GET_APP_METADATA"; field public static final String GET_APP_OPS_STATS = "android.permission.GET_APP_OPS_STATS"; + field @FlaggedApi("android.app.get_binding_uid_importance") public static final String GET_BINDING_UID_IMPORTANCE = "android.permission.GET_BINDING_UID_IMPORTANCE"; field public static final String GET_HISTORICAL_APP_OPS_STATS = "android.permission.GET_HISTORICAL_APP_OPS_STATS"; field public static final String GET_PROCESS_STATE_AND_OOM_SCORE = "android.permission.GET_PROCESS_STATE_AND_OOM_SCORE"; field public static final String GET_RUNTIME_PERMISSIONS = "android.permission.GET_RUNTIME_PERMISSIONS"; @@ -542,6 +543,7 @@ package android.app { public class ActivityManager { method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void addOnUidImportanceListener(android.app.ActivityManager.OnUidImportanceListener, int); method @RequiresPermission(android.Manifest.permission.FORCE_STOP_PACKAGES) public void forceStopPackage(String); + method @FlaggedApi("android.app.get_binding_uid_importance") @RequiresPermission(android.Manifest.permission.GET_BINDING_UID_IMPORTANCE) public int getBindingUidImportance(int); method @RequiresPermission(anyOf={"android.permission.INTERACT_ACROSS_USERS", "android.permission.INTERACT_ACROSS_USERS_FULL"}) public static int getCurrentUser(); method @FlaggedApi("android.app.app_start_info") @NonNull @RequiresPermission(android.Manifest.permission.DUMP) public java.util.List getExternalHistoricalProcessStartReasons(@NonNull String, @IntRange(from=0) int); method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public int getPackageImportance(String); diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index f68681b54e48..8b4ebaee04c5 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -73,7 +73,6 @@ import android.os.PowerExemptionManager.ReasonCode; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; -import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; @@ -4268,6 +4267,33 @@ public class ActivityManager { } } + /** + * Same as {@link #getUidImportance(int)}, but it only works on UIDs that currently + * have a service binding, or provider reference, to the calling UID, even if the target UID + * belong to another android user or profile. + * + *

This will return {@link RunningAppProcessInfo#IMPORTANCE_GONE} on all other UIDs, + * regardless of if they're valid or not. + * + *

Privileged system apps may prefer this API to {@link #getUidImportance(int)} to + * avoid requesting the permission {@link Manifest.permission#PACKAGE_USAGE_STATS}, which + * would allow access to APIs that return more senstive information. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_GET_BINDING_UID_IMPORTANCE) + @SystemApi + @RequiresPermission(Manifest.permission.GET_BINDING_UID_IMPORTANCE) + public @RunningAppProcessInfo.Importance int getBindingUidImportance(int uid) { + try { + int procState = getService().getBindingUidProcessState(uid, + mContext.getOpPackageName()); + return RunningAppProcessInfo.procStateToImportanceForClient(procState, mContext); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Callback to get reports about changes to the importance of a uid. Use with * {@link #addOnUidImportanceListener}. diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 520bf7dc890c..260e9859c72d 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -949,4 +949,5 @@ interface IActivityManager { * @param err The binder transaction error */ oneway void frozenBinderTransactionDetected(int debugPid, int code, int flags, int err); + int getBindingUidProcessState(int uid, in String callingPackage); } diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig index 2076e85828a6..b303ea64406c 100644 --- a/core/java/android/app/activity_manager.aconfig +++ b/core/java/android/app/activity_manager.aconfig @@ -5,4 +5,11 @@ flag { name: "app_start_info" description: "Control collecting of ApplicationStartInfo records and APIs." bug: "247814855" -} \ No newline at end of file +} + +flag { + namespace: "backstage_power" + name: "get_binding_uid_importance" + description: "API to get importance of UID that's binding to the caller" + bug: "292533010" +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 8c91be8b21c0..25958eed3f0b 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -7765,6 +7765,15 @@ + + + + diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 10d04d3ff6b3..5bd37c397d0e 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -863,6 +863,7 @@ + { + if (pr.uid == callingUid) { + final ProcessServiceRecord psr = pr.mServices; + final int serviceCount = psr.mServices.size(); + for (int svc = 0; svc < serviceCount; svc++) { + final ArrayMap> conns = + psr.mServices.valueAt(svc).getConnections(); + final int size = conns.size(); + for (int conni = 0; conni < size; conni++) { + final ArrayList crs = conns.valueAt(conni); + for (int con = 0; con < crs.size(); con++) { + final ConnectionRecord cr = crs.get(con); + final ProcessRecord clientPr = cr.binding.client; + + if (clientPr.uid == clientUid) { + return Boolean.TRUE; + } + } + } + } + } + return null; + }); + if (Boolean.TRUE.equals(hasBinding)) { + return true; + } + + final Boolean hasProviderClient = mProcessList.searchEachLruProcessesLOSP( + false, pr -> { + if (pr.uid == callingUid) { + final ProcessProviderRecord ppr = pr.mProviders; + for (int provi = ppr.numberOfProviders() - 1; provi >= 0; provi--) { + ContentProviderRecord cpr = ppr.getProviderAt(provi); + + for (int i = cpr.connections.size() - 1; i >= 0; i--) { + ContentProviderConnection conn = cpr.connections.get(i); + ProcessRecord client = conn.client; + if (client.uid == clientUid) { + return Boolean.TRUE; + } + } + } + } + return null; + }); + + return Boolean.TRUE.equals(hasProviderClient); + } + @Override public @ProcessCapability int getUidProcessCapabilities(int uid, String callingPackage) { if (!hasUsageStatsPermission(callingPackage)) { -- cgit v1.2.3-59-g8ed1b From ad6d6b6c90e1cd2acc16959a48b0016f5686c87a Mon Sep 17 00:00:00 2001 From: Kangping Dong Date: Thu, 3 Aug 2023 19:11:51 +0800 Subject: [Thread] add Thread network permissions Defines permissions for guarding access to Thread network API. The permissions are also granted to shell for CTS tests, see the instructions here: go/ctswrite#write-a-test-to-test-an-api-that-requires-system-permission Design doc: go/thread-android-api (cherry picked from commit 03aa71cd4b5eabe9310d3c4d4db10038b80853d1) Bug: 262683651 Change-Id: Ib15d5589bbd304d68840ee52a193676d12110512 --- core/api/system-current.txt | 1 + core/res/AndroidManifest.xml | 7 +++++++ data/etc/privapp-permissions-platform.xml | 2 ++ packages/Shell/AndroidManifest.xml | 3 +++ 4 files changed, 13 insertions(+) (limited to 'data') diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 441dcaea5e54..e6daaa7a81f8 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -367,6 +367,7 @@ package android { field public static final String SYSTEM_APPLICATION_OVERLAY = "android.permission.SYSTEM_APPLICATION_OVERLAY"; field public static final String SYSTEM_CAMERA = "android.permission.SYSTEM_CAMERA"; field public static final String TETHER_PRIVILEGED = "android.permission.TETHER_PRIVILEGED"; + field @FlaggedApi("com.android.net.thread.flags.thread_enabled") public static final String THREAD_NETWORK_PRIVILEGED = "android.permission.THREAD_NETWORK_PRIVILEGED"; field public static final String TIS_EXTENSION_INTERFACE = "android.permission.TIS_EXTENSION_INTERFACE"; field public static final String TOGGLE_AUTOMOTIVE_PROJECTION = "android.permission.TOGGLE_AUTOMOTIVE_PROJECTION"; field public static final String TRIGGER_LOST_MODE = "android.permission.TRIGGER_LOST_MODE"; diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index ab0ef7d3b187..79da6ad620cb 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2247,6 +2247,13 @@ + + + diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 69aa40156e78..ad3c31b5acc6 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -439,6 +439,8 @@ applications that come with the platform + + diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index c7e5bf98850a..09fdc85ebf0f 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -592,6 +592,9 @@ + + + -- cgit v1.2.3-59-g8ed1b From 7c64d6873a5f72cb11a6a62ab5771c5a50eed14c Mon Sep 17 00:00:00 2001 From: wilsonshih Date: Wed, 8 Nov 2023 07:19:40 +0000 Subject: Refine back navigation search method. - Remove embedded window check, EmbeddedWindowController#getByFocusToken should search with input token instead of window token, so it always return null nowaday. - Skip search if the closing target isn't visibleRequested, usually that means the closing transition is collecting or playing, either case the window shouldn't receive another back invoked event. Inject the back key then next focus app will handle it. - Use TYPE_CALLBACK if we cannot find any task below current task, this could also happen on some specific device.(e.g. No home) Bug: 303266152 Bug: 309683765 Test: atest BackNavigationControllerTests Change-Id: Ia1f5ec664ea7154d9c8ac59cbc247a51226dfdf1 --- data/etc/services.core.protolog.json | 30 +---- .../server/wm/BackNavigationController.java | 133 +++++++++------------ .../server/wm/BackNavigationControllerTests.java | 14 +++ 3 files changed, 76 insertions(+), 101 deletions(-) (limited to 'data') diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index dc2b0561957d..b06561101071 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -415,12 +415,6 @@ "group": "WM_DEBUG_WINDOW_TRANSITIONS", "at": "com\/android\/server\/wm\/Transition.java" }, - "-1717147904": { - "message": "Current focused window is embeddedWindow. Dispatch KEYCODE_BACK.", - "level": "DEBUG", - "group": "WM_DEBUG_BACK_PREVIEW", - "at": "com\/android\/server\/wm\/BackNavigationController.java" - }, "-1710206702": { "message": "Display id=%d is frozen while keyguard locked, return %d", "level": "VERBOSE", @@ -1213,12 +1207,6 @@ "group": "WM_DEBUG_STATES", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, - "-997565097": { - "message": "Focused window found using getFocusedWindowToken", - "level": "DEBUG", - "group": "WM_DEBUG_BACK_PREVIEW", - "at": "com\/android\/server\/wm\/BackNavigationController.java" - }, "-993378225": { "message": "finishDrawingLocked: mDrawState=COMMIT_DRAW_PENDING %s in %s", "level": "VERBOSE", @@ -2233,6 +2221,12 @@ "group": "WM_DEBUG_RECENTS_ANIMATIONS", "at": "com\/android\/server\/wm\/RecentsAnimation.java" }, + "-98422345": { + "message": "Focus window is closing.", + "level": "DEBUG", + "group": "WM_DEBUG_BACK_PREVIEW", + "at": "com\/android\/server\/wm\/BackNavigationController.java" + }, "-91393839": { "message": "Set animatingExit: reason=remove\/applyAnimation win=%s", "level": "VERBOSE", @@ -2731,12 +2725,6 @@ "group": "WM_DEBUG_STATES", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, - "309039362": { - "message": "SURFACE MATRIX [%f,%f,%f,%f]: %s", - "level": "INFO", - "group": "WM_SHOW_TRANSACTIONS", - "at": "com\/android\/server\/wm\/WindowSurfaceController.java" - }, "312030608": { "message": "New topFocusedDisplayId=%d", "level": "DEBUG", @@ -3091,12 +3079,6 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, - "633654009": { - "message": "SURFACE POS (setPositionInTransaction) @ (%f,%f): %s", - "level": "INFO", - "group": "WM_SHOW_TRANSACTIONS", - "at": "com\/android\/server\/wm\/WindowSurfaceController.java" - }, "638429464": { "message": "\tRemove container=%s", "level": "DEBUG", diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 87ae045d4f12..43f32096b6c0 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -39,7 +39,6 @@ import android.content.res.ResourceId; import android.graphics.Point; import android.graphics.Rect; import android.os.Bundle; -import android.os.IBinder; import android.os.RemoteCallback; import android.os.RemoteException; import android.os.SystemProperties; @@ -60,7 +59,6 @@ import android.window.TaskSnapshot; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.common.ProtoLog; -import com.android.server.LocalServices; import com.android.server.wm.utils.InsetUtils; import java.io.PrintWriter; @@ -151,97 +149,77 @@ class BackNavigationController { // Don't start any animation for it. return null; } - WindowManagerInternal windowManagerInternal = - LocalServices.getService(WindowManagerInternal.class); - IBinder focusedWindowToken = windowManagerInternal.getFocusedWindowToken(); window = wmService.getFocusedWindowLocked(); - if (window == null) { - EmbeddedWindowController.EmbeddedWindow embeddedWindow = - wmService.mEmbeddedWindowController.getByInputTransferToken( - focusedWindowToken); - if (embeddedWindow != null) { - ProtoLog.d(WM_DEBUG_BACK_PREVIEW, - "Current focused window is embeddedWindow. Dispatch KEYCODE_BACK."); - return null; - } - } - - // Lets first gather the states of things - // - What is our current window ? - // - Does it has an Activity and a Task ? - // TODO Temp workaround for Sysui until b/221071505 is fixed - if (window != null) { - ProtoLog.d(WM_DEBUG_BACK_PREVIEW, - "Focused window found using getFocusedWindowToken"); - } - - if (window != null) { - // This is needed to bridge the old and new back behavior with recents. While in - // Overview with live tile enabled, the previous app is technically focused but we - // add an input consumer to capture all input that would otherwise go to the apps - // being controlled by the animation. This means that the window resolved is not - // the right window to consume back while in overview, so we need to route it to - // launcher and use the legacy behavior of injecting KEYCODE_BACK since the existing - // compat callback in VRI only works when the window is focused. - // This symptom also happen while shell transition enabled, we can check that by - // isTransientLaunch to know whether the focus window is point to live tile. - final RecentsAnimationController recentsAnimationController = - wmService.getRecentsAnimationController(); - final ActivityRecord ar = window.mActivityRecord; - if ((ar != null && ar.isActivityTypeHomeOrRecents() - && ar.mTransitionController.isTransientLaunch(ar)) - || (recentsAnimationController != null - && recentsAnimationController.shouldApplyInputConsumer(ar))) { - ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Current focused window being animated by " - + "recents. Overriding back callback to recents controller callback."); - return null; - } - - if (!window.isDrawn()) { - ProtoLog.d(WM_DEBUG_BACK_PREVIEW, - "Focused window didn't have a valid surface drawn."); - return null; - } - } - if (window == null) { // We don't have any focused window, fallback ont the top currentTask of the focused // display. ProtoLog.w(WM_DEBUG_BACK_PREVIEW, "No focused window, defaulting to top current task's window"); currentTask = wmService.mAtmService.getTopDisplayFocusedRootTask(); - window = currentTask.getWindow(WindowState::isFocused); + window = currentTask != null + ? currentTask.getWindow(WindowState::isFocused) : null; + } + + if (window == null) { + Slog.e(TAG, "Window is null, returning null."); + return null; } + // This is needed to bridge the old and new back behavior with recents. While in + // Overview with live tile enabled, the previous app is technically focused but we + // add an input consumer to capture all input that would otherwise go to the apps + // being controlled by the animation. This means that the window resolved is not + // the right window to consume back while in overview, so we need to route it to + // launcher and use the legacy behavior of injecting KEYCODE_BACK since the existing + // compat callback in VRI only works when the window is focused. + // This symptom also happen while shell transition enabled, we can check that by + // isTransientLaunch to know whether the focus window is point to live tile. + final RecentsAnimationController recentsAnimationController = + wmService.getRecentsAnimationController(); + final ActivityRecord tmpAR = window.mActivityRecord; + if ((tmpAR != null && tmpAR.isActivityTypeHomeOrRecents() + && tmpAR.mTransitionController.isTransientLaunch(tmpAR)) + || (recentsAnimationController != null + && recentsAnimationController.shouldApplyInputConsumer(tmpAR))) { + ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Current focused window being animated by " + + "recents. Overriding back callback to recents controller callback."); + return null; + } + + if (!window.isDrawn()) { + ProtoLog.d(WM_DEBUG_BACK_PREVIEW, + "Focused window didn't have a valid surface drawn."); + return null; + } + + currentActivity = window.mActivityRecord; + currentTask = window.getTask(); + if ((currentTask != null && !currentTask.isVisibleRequested()) + || (currentActivity != null && !currentActivity.isVisibleRequested())) { + // Closing transition is happening on focus window and should be update soon, + // don't drive back navigation with it. + ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Focus window is closing."); + return null; + } // Now let's find if this window has a callback from the client side. - OnBackInvokedCallbackInfo callbackInfo = null; - if (window != null) { - currentActivity = window.mActivityRecord; - currentTask = window.getTask(); - callbackInfo = window.getOnBackInvokedCallbackInfo(); - if (callbackInfo == null) { - Slog.e(TAG, "No callback registered, returning null."); - return null; - } - if (!callbackInfo.isSystemCallback()) { - backType = BackNavigationInfo.TYPE_CALLBACK; - } - infoBuilder.setOnBackInvokedCallback(callbackInfo.getCallback()); - infoBuilder.setAnimationCallback(callbackInfo.isAnimationCallback()); - mNavigationMonitor.startMonitor(window, navigationObserver); + final OnBackInvokedCallbackInfo callbackInfo = window.getOnBackInvokedCallbackInfo(); + if (callbackInfo == null) { + Slog.e(TAG, "No callback registered, returning null."); + return null; } + if (!callbackInfo.isSystemCallback()) { + backType = BackNavigationInfo.TYPE_CALLBACK; + } + infoBuilder.setOnBackInvokedCallback(callbackInfo.getCallback()); + infoBuilder.setAnimationCallback(callbackInfo.isAnimationCallback()); + mNavigationMonitor.startMonitor(window, navigationObserver); ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "startBackNavigation currentTask=%s, " + "topRunningActivity=%s, callbackInfo=%s, currentFocus=%s", currentTask, currentActivity, callbackInfo, window); - if (window == null) { - Slog.e(TAG, "Window is null, returning null."); - return null; - } - // If we don't need to set up the animation, we return early. This is the case when // - We have an application callback. // - We don't have any ActivityRecord or Task to animate. @@ -322,12 +300,13 @@ class BackNavigationController { } return false; }, currentTask, false /*includeBoundary*/, true /*traverseTopToBottom*/); - final ActivityRecord tmpPre = prevTask.getTopNonFinishingActivity(); + final ActivityRecord tmpPre = prevTask != null + ? prevTask.getTopNonFinishingActivity() : null; if (tmpPre != null) { prevActivities.add(tmpPre); findAdjacentActivityIfExist(tmpPre, prevActivities); } - if (prevActivities.isEmpty() + if (prevTask == null || prevActivities.isEmpty() || (isOccluded && !prevActivities.get(0).canShowWhenLocked())) { backType = BackNavigationInfo.TYPE_CALLBACK; } else if (prevTask.isActivityTypeHome()) { diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index 6790dc2e8733..afea8114d508 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -116,6 +116,20 @@ public class BackNavigationControllerTests extends WindowTestsBase { assertTrue("Animation scheduled", backNavigationInfo.isPrepareRemoteAnimation()); } + @Test + public void noBackWhenMoveTaskToBack() { + Task taskA = createTask(mDefaultDisplay); + ActivityRecord recordA = createActivityRecord(taskA); + Mockito.doNothing().when(recordA).reparentSurfaceControl(any(), any()); + + final Task topTask = createTopTaskWithActivity(); + withSystemCallback(topTask); + // simulate moveTaskToBack + topTask.setVisibleRequested(false); + BackNavigationInfo backNavigationInfo = startBackNavigation(); + assertWithMessage("BackNavigationInfo").that(backNavigationInfo).isNull(); + } + @Test public void backTypeCrossTaskWhenBackToPreviousTask() { Task taskA = createTask(mDefaultDisplay); -- cgit v1.2.3-59-g8ed1b From b2cc0811772c909f6d35f6aca6785103ea5c9f70 Mon Sep 17 00:00:00 2001 From: Vladimir Komsiyski Date: Tue, 14 Nov 2023 09:23:37 +0100 Subject: Support for KEYCODE_LANGUAGE_SWITCH in VirtualKeyboard Fix: 300385158 Test: presubmit Test: manual, verified that KEYCODE_LANGUAGE_SWITCH reaches the framework Change-Id: If98addb0f9f3cff93bb0159b8fde171acf6a3998 --- core/java/android/hardware/input/VirtualKeyEvent.java | 1 + data/keyboards/Generic.kl | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'data') diff --git a/core/java/android/hardware/input/VirtualKeyEvent.java b/core/java/android/hardware/input/VirtualKeyEvent.java index dc47f0820582..8ed535e74b2b 100644 --- a/core/java/android/hardware/input/VirtualKeyEvent.java +++ b/core/java/android/hardware/input/VirtualKeyEvent.java @@ -172,6 +172,7 @@ public final class VirtualKeyEvent implements Parcelable { KeyEvent.KEYCODE_BREAK, KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_FORWARD, + KeyEvent.KEYCODE_LANGUAGE_SWITCH, }) @Retention(RetentionPolicy.SOURCE) public @interface SupportedKeycode { diff --git a/data/keyboards/Generic.kl b/data/keyboards/Generic.kl index 51b720ddb758..f9347ee6ea09 100644 --- a/data/keyboards/Generic.kl +++ b/data/keyboards/Generic.kl @@ -324,7 +324,7 @@ key 362 GUIDE # key 365 "KEY_EPG" key 366 DVR # key 367 "KEY_MHP" -# key 368 "KEY_LANGUAGE" +key 368 LANGUAGE_SWITCH # key 369 "KEY_TITLE" key 370 CAPTIONS # key 371 "KEY_ANGLE" -- cgit v1.2.3-59-g8ed1b From 410361a9ed017976df8fc9699784f37a3f29d2f2 Mon Sep 17 00:00:00 2001 From: Vladimir Komsiyski Date: Tue, 26 Sep 2023 16:41:25 +0200 Subject: API for home support on virtual displays. Instead of adding yet another virtual display flag, the API is in VirtualDisplayConfig and uses WM's DisplayWindowSettings to store the bit whether home is supported. The difference with the existing FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS is that it also adds navigation bar and the new API doesn't. The flag is hidden but there are existing clients of it. Several caveats: - Need to use displayUniqueId instead of displayId because we should tell WM about the home support before the display is actually created and the display listeners notified. - Interacting with the DisplayWindowSettings requires the WM lock, which must not be acquired while DisplayManagerService is holding its own lock because this may and will sometimes cause deadlock. - So extracting the displayUniqueId generation logic before the DMS locked region of actually creating the virtual display and passing it to WM to store the settings. - Change in the virtual display uniqueId generation: reusing ids per package/uid causes problems in when displays with the same name are created and released quickly (CTS). When there are no display devices, the same unique id is used, but the DisplayWindowSettings may not have yet received the previous onDisplayRemoved callback, so the setting for that uniqueId is removed. Making the uniqueIds truly unique fixes this and there's no realistic danger of an overflow. Fix: 291749213 Fix: 297167917 Test: see CTS in topic Test: atest VirtualDisplayAdapterTest Test: atest DisplayWindowSettingsTests Test: atest DisplayAreaPolicyTests Change-Id: If72696a793a9c4d63d4f8b72de7433b0dd440909 --- core/api/system-current.txt | 8 +++ .../companion/virtual/VirtualDeviceParams.java | 6 ++- .../hardware/display/VirtualDisplayConfig.java | 52 ++++++++++++++++-- data/etc/services.core.protolog.json | 6 +++ .../server/display/DisplayManagerService.java | 25 ++++++++- .../server/display/VirtualDisplayAdapter.java | 62 ++++++++-------------- .../server/wallpaper/WallpaperDisplayHelper.java | 2 +- .../java/com/android/server/wm/DisplayContent.java | 14 ++++- .../android/server/wm/DisplayWindowSettings.java | 60 +++++++++++++++++++-- .../server/wm/DisplayWindowSettingsProvider.java | 11 ++++ .../com/android/server/wm/RootWindowContainer.java | 11 ++-- .../com/android/server/wm/TaskDisplayArea.java | 2 +- .../android/server/wm/WindowManagerInternal.java | 27 +++++++++- .../android/server/wm/WindowManagerService.java | 36 ++++++++++++- .../server/display/DisplayManagerServiceTest.java | 3 +- .../server/display/VirtualDisplayAdapterTest.java | 8 +-- .../android/server/wm/DisplayAreaPolicyTests.java | 2 +- .../server/wm/DisplayWindowSettingsTests.java | 18 +++++++ 18 files changed, 284 insertions(+), 69 deletions(-) (limited to 'data') diff --git a/core/api/system-current.txt b/core/api/system-current.txt index a7ea753cb422..afea9b53b49e 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -4598,6 +4598,14 @@ package android.hardware.display { field public static final int VIRTUAL_DISPLAY_FLAG_TRUSTED = 1024; // 0x400 } + public final class VirtualDisplayConfig implements android.os.Parcelable { + method @FlaggedApi("android.companion.virtual.flags.vdm_custom_home") public boolean isHomeSupported(); + } + + public static final class VirtualDisplayConfig.Builder { + method @FlaggedApi("android.companion.virtual.flags.vdm_custom_home") @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setHomeSupported(boolean); + } + } package android.hardware.hdmi { diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java index 97a7aa4a3bea..0d73e44f5197 100644 --- a/core/java/android/companion/virtual/VirtualDeviceParams.java +++ b/core/java/android/companion/virtual/VirtualDeviceParams.java @@ -37,6 +37,7 @@ import android.companion.virtual.sensor.VirtualSensorConfig; import android.companion.virtual.sensor.VirtualSensorDirectChannelCallback; import android.content.ComponentName; import android.content.Context; +import android.hardware.display.VirtualDisplayConfig; import android.os.Parcel; import android.os.Parcelable; import android.os.SharedMemory; @@ -326,8 +327,8 @@ public final class VirtualDeviceParams implements Parcelable { * support home activities. * * @see Builder#setHomeComponent + * @see VirtualDisplayConfig#isHomeSupported() */ - // TODO(b/297168328): Link to the relevant API for creating displays with home support. @FlaggedApi(Flags.FLAG_VDM_CUSTOM_HOME) @Nullable public ComponentName getHomeComponent() { @@ -737,8 +738,9 @@ public final class VirtualDeviceParams implements Parcelable { * * @param homeComponent The component name to be used as home. If unset, then the system- * default secondary home activity will be used. + * + * @see VirtualDisplayConfig#isHomeSupported() */ - // TODO(b/297168328): Link to the relevant API for creating displays with home support. @FlaggedApi(Flags.FLAG_VDM_CUSTOM_HOME) @NonNull public Builder setHomeComponent(@Nullable ComponentName homeComponent) { diff --git a/core/java/android/hardware/display/VirtualDisplayConfig.java b/core/java/android/hardware/display/VirtualDisplayConfig.java index 22e3938d3818..7388b5bb1495 100644 --- a/core/java/android/hardware/display/VirtualDisplayConfig.java +++ b/core/java/android/hardware/display/VirtualDisplayConfig.java @@ -18,10 +18,12 @@ package android.hardware.display; import static android.view.Display.DEFAULT_DISPLAY; +import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.hardware.display.DisplayManager.VirtualDisplayFlag; import android.media.projection.MediaProjection; import android.os.Handler; @@ -55,6 +57,7 @@ public final class VirtualDisplayConfig implements Parcelable { private final boolean mWindowManagerMirroringEnabled; private ArraySet mDisplayCategories = null; private final float mRequestedRefreshRate; + private final boolean mIsHomeSupported; private VirtualDisplayConfig( @NonNull String name, @@ -67,7 +70,8 @@ public final class VirtualDisplayConfig implements Parcelable { int displayIdToMirror, boolean windowManagerMirroringEnabled, @NonNull ArraySet displayCategories, - float requestedRefreshRate) { + float requestedRefreshRate, + boolean isHomeSupported) { mName = name; mWidth = width; mHeight = height; @@ -79,6 +83,7 @@ public final class VirtualDisplayConfig implements Parcelable { mWindowManagerMirroringEnabled = windowManagerMirroringEnabled; mDisplayCategories = displayCategories; mRequestedRefreshRate = requestedRefreshRate; + mIsHomeSupported = isHomeSupported; } /** @@ -156,6 +161,18 @@ public final class VirtualDisplayConfig implements Parcelable { return mWindowManagerMirroringEnabled; } + /** + * Whether this virtual display supports showing home activity and wallpaper. + * + * @see Builder#setHomeSupported + * @hide + */ + @FlaggedApi(android.companion.virtual.flags.Flags.FLAG_VDM_CUSTOM_HOME) + @SystemApi + public boolean isHomeSupported() { + return android.companion.virtual.flags.Flags.vdmCustomHome() && mIsHomeSupported; + } + /** * Returns the display categories. * @@ -189,6 +206,7 @@ public final class VirtualDisplayConfig implements Parcelable { dest.writeBoolean(mWindowManagerMirroringEnabled); dest.writeArraySet(mDisplayCategories); dest.writeFloat(mRequestedRefreshRate); + dest.writeBoolean(mIsHomeSupported); } @Override @@ -213,7 +231,8 @@ public final class VirtualDisplayConfig implements Parcelable { && mDisplayIdToMirror == that.mDisplayIdToMirror && mWindowManagerMirroringEnabled == that.mWindowManagerMirroringEnabled && Objects.equals(mDisplayCategories, that.mDisplayCategories) - && mRequestedRefreshRate == that.mRequestedRefreshRate; + && mRequestedRefreshRate == that.mRequestedRefreshRate + && mIsHomeSupported == that.mIsHomeSupported; } @Override @@ -221,7 +240,7 @@ public final class VirtualDisplayConfig implements Parcelable { int hashCode = Objects.hash( mName, mWidth, mHeight, mDensityDpi, mFlags, mSurface, mUniqueId, mDisplayIdToMirror, mWindowManagerMirroringEnabled, mDisplayCategories, - mRequestedRefreshRate); + mRequestedRefreshRate, mIsHomeSupported); return hashCode; } @@ -240,6 +259,7 @@ public final class VirtualDisplayConfig implements Parcelable { + " mWindowManagerMirroringEnabled=" + mWindowManagerMirroringEnabled + " mDisplayCategories=" + mDisplayCategories + " mRequestedRefreshRate=" + mRequestedRefreshRate + + " mIsHomeSupported=" + mIsHomeSupported + ")"; } @@ -255,6 +275,7 @@ public final class VirtualDisplayConfig implements Parcelable { mWindowManagerMirroringEnabled = in.readBoolean(); mDisplayCategories = (ArraySet) in.readArraySet(null); mRequestedRefreshRate = in.readFloat(); + mIsHomeSupported = in.readBoolean(); } @NonNull @@ -286,6 +307,7 @@ public final class VirtualDisplayConfig implements Parcelable { private boolean mWindowManagerMirroringEnabled = false; private ArraySet mDisplayCategories = new ArraySet<>(); private float mRequestedRefreshRate = 0.0f; + private boolean mIsHomeSupported = false; /** * Creates a new Builder. @@ -421,6 +443,27 @@ public final class VirtualDisplayConfig implements Parcelable { return this; } + /** + * Sets whether this display supports showing home activities and wallpaper. + * + *

If set to {@code true}, then the home activity relevant to this display will be + * automatically launched upon the display creation.

+ * + *

Note: setting to {@code true} requires the display to be trusted. If the display is + * not trusted, this property is ignored.

+ * + * @param isHomeSupported whether home activities are supported on the display + * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED + * @hide + */ + @FlaggedApi(android.companion.virtual.flags.Flags.FLAG_VDM_CUSTOM_HOME) + @SystemApi + @NonNull + public Builder setHomeSupported(boolean isHomeSupported) { + mIsHomeSupported = isHomeSupported; + return this; + } + /** * Builds the {@link VirtualDisplayConfig} instance. */ @@ -437,7 +480,8 @@ public final class VirtualDisplayConfig implements Parcelable { mDisplayIdToMirror, mWindowManagerMirroringEnabled, mDisplayCategories, - mRequestedRefreshRate); + mRequestedRefreshRate, + mIsHomeSupported); } } } diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index b06561101071..f19acbe023b8 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -811,6 +811,12 @@ "group": "WM_DEBUG_STARTING_WINDOW", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "-1325565952": { + "message": "Attempted to get home support flag of a display that does not exist: %d", + "level": "WARN", + "group": "WM_ERROR", + "at": "com\/android\/server\/wm\/WindowManagerService.java" + }, "-1323783276": { "message": "performEnableScreen: bootFinished() failed.", "level": "WARN", diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index eae153cb8aeb..304bffb18a96 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -1607,6 +1607,19 @@ public final class DisplayManagerService extends SystemService { final long secondToken = Binder.clearCallingIdentity(); try { final int displayId; + final String displayUniqueId = VirtualDisplayAdapter.generateDisplayUniqueId( + packageName, callingUid, virtualDisplayConfig); + + if (virtualDisplayConfig.isHomeSupported()) { + if ((flags & VIRTUAL_DISPLAY_FLAG_TRUSTED) == 0) { + Slog.w(TAG, "Display created with home support but lacks " + + "VIRTUAL_DISPLAY_FLAG_TRUSTED, ignoring the home support request."); + } else { + mWindowManagerInternal.setHomeSupportedOnDisplay(displayUniqueId, + Display.TYPE_VIRTUAL, true); + } + } + synchronized (mSyncRoot) { displayId = createVirtualDisplayLocked( @@ -1614,6 +1627,7 @@ public final class DisplayManagerService extends SystemService { projection, callingUid, packageName, + displayUniqueId, virtualDevice, surface, flags, @@ -1625,6 +1639,13 @@ public final class DisplayManagerService extends SystemService { } } + if (displayId == Display.INVALID_DISPLAY && virtualDisplayConfig.isHomeSupported() + && (flags & VIRTUAL_DISPLAY_FLAG_TRUSTED) != 0) { + // Failed to create the virtual display, so we should clean up the WM settings + // because it won't receive the onDisplayRemoved callback. + mWindowManagerInternal.clearDisplaySettings(displayUniqueId, Display.TYPE_VIRTUAL); + } + // Build a session describing the MediaProjection instance, if there is one. A session // for a VirtualDisplay or physical display mirroring is handled in DisplayContent. ContentRecordingSession session = null; @@ -1698,6 +1719,7 @@ public final class DisplayManagerService extends SystemService { IMediaProjection projection, int callingUid, String packageName, + String uniqueId, IVirtualDevice virtualDevice, Surface surface, int flags, @@ -1710,10 +1732,9 @@ public final class DisplayManagerService extends SystemService { return -1; } - Slog.d(TAG, "Virtual Display: creating DisplayDevice with VirtualDisplayAdapter"); DisplayDevice device = mVirtualDisplayAdapter.createVirtualDisplayLocked( - callback, projection, callingUid, packageName, surface, flags, + callback, projection, callingUid, packageName, uniqueId, surface, flags, virtualDisplayConfig); if (device == null) { Slog.w(TAG, "Virtual Display: VirtualDisplayAdapter failed to create DisplayDevice"); diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java index b0025872aa3d..90e32a685a34 100644 --- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java +++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java @@ -64,7 +64,7 @@ import android.view.SurfaceControl; import com.android.internal.annotations.VisibleForTesting; import java.io.PrintWriter; -import java.util.Iterator; +import java.util.concurrent.atomic.AtomicInteger; /** * A display adapter that provides virtual displays on behalf of applications. @@ -72,15 +72,17 @@ import java.util.Iterator; * Display adapters are guarded by the {@link DisplayManagerService.SyncRoot} lock. *

*/ -@VisibleForTesting +@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public class VirtualDisplayAdapter extends DisplayAdapter { static final String TAG = "VirtualDisplayAdapter"; - static final boolean DEBUG = false; // Unique id prefix for virtual displays @VisibleForTesting static final String UNIQUE_ID_PREFIX = "virtual:"; + // Unique id suffix for virtual displays + private static final AtomicInteger sNextUniqueIndex = new AtomicInteger(0); + private final ArrayMap mVirtualDisplayDevices = new ArrayMap<>(); private final Handler mHandler; private final SurfaceControlDisplayFactory mSurfaceControlDisplayFactory; @@ -111,8 +113,8 @@ public class VirtualDisplayAdapter extends DisplayAdapter { } public DisplayDevice createVirtualDisplayLocked(IVirtualDisplayCallback callback, - IMediaProjection projection, int ownerUid, String ownerPackageName, Surface surface, - int flags, VirtualDisplayConfig virtualDisplayConfig) { + IMediaProjection projection, int ownerUid, String ownerPackageName, String uniqueId, + Surface surface, int flags, VirtualDisplayConfig virtualDisplayConfig) { IBinder appToken = callback.asBinder(); if (mVirtualDisplayDevices.containsKey(appToken)) { Slog.wtfStack(TAG, @@ -125,23 +127,13 @@ public class VirtualDisplayAdapter extends DisplayAdapter { IBinder displayToken = mSurfaceControlDisplayFactory.createDisplay(name, secure, virtualDisplayConfig.getRequestedRefreshRate()); - final String baseUniqueId = - UNIQUE_ID_PREFIX + ownerPackageName + "," + ownerUid + "," + name + ","; - final int uniqueIndex = getNextUniqueIndex(baseUniqueId); - String uniqueId = virtualDisplayConfig.getUniqueId(); - if (uniqueId == null) { - uniqueId = baseUniqueId + uniqueIndex; - } else { - uniqueId = UNIQUE_ID_PREFIX + ownerPackageName + ":" + uniqueId; - } MediaProjectionCallback mediaProjectionCallback = null; if (projection != null) { mediaProjectionCallback = new MediaProjectionCallback(appToken); } VirtualDisplayDevice device = new VirtualDisplayDevice(displayToken, appToken, - ownerUid, ownerPackageName, surface, flags, - new Callback(callback, mHandler), projection, mediaProjectionCallback, - uniqueId, uniqueIndex, virtualDisplayConfig); + ownerUid, ownerPackageName, surface, flags, new Callback(callback, mHandler), + projection, mediaProjectionCallback, uniqueId, virtualDisplayConfig); mVirtualDisplayDevices.put(appToken, device); @@ -219,26 +211,20 @@ public class VirtualDisplayAdapter extends DisplayAdapter { } /** - * Returns the next unique index for the uniqueIdPrefix + * Generates a virtual display's unique identifier. + * + *

It is always prefixed with "virtual:package-name". If the provided config explicitly + * specifies a unique ID, then it's simply appended. Otherwise, the UID, display name and a + * unique index are appended.

+ * + *

The unique index is incremented for every virtual display unique ID generation and serves + * for differentiating between displays with the same name created by the same owner.

*/ - private int getNextUniqueIndex(String uniqueIdPrefix) { - if (mVirtualDisplayDevices.isEmpty()) { - return 0; - } - - int nextUniqueIndex = 0; - Iterator it = mVirtualDisplayDevices.values().iterator(); - while (it.hasNext()) { - VirtualDisplayDevice device = it.next(); - if (device.getUniqueId().startsWith(uniqueIdPrefix) - && device.mUniqueIndex >= nextUniqueIndex) { - // Increment the next unique index to be greater than ones we have already ran - // across for displays that have the same unique Id prefix. - nextUniqueIndex = device.mUniqueIndex + 1; - } - } - - return nextUniqueIndex; + static String generateDisplayUniqueId(String packageName, int uid, + VirtualDisplayConfig config) { + return UNIQUE_ID_PREFIX + packageName + ((config.getUniqueId() != null) + ? (":" + config.getUniqueId()) + : ("," + uid + "," + config.getName() + "," + sNextUniqueIndex.getAndIncrement())); } private void handleBinderDiedLocked(IBinder appToken) { @@ -278,7 +264,6 @@ public class VirtualDisplayAdapter extends DisplayAdapter { private int mDisplayState; private boolean mStopped; private int mPendingChanges; - private int mUniqueIndex; private Display.Mode mMode; private boolean mIsDisplayOn; private int mDisplayIdToMirror; @@ -287,7 +272,7 @@ public class VirtualDisplayAdapter extends DisplayAdapter { public VirtualDisplayDevice(IBinder displayToken, IBinder appToken, int ownerUid, String ownerPackageName, Surface surface, int flags, Callback callback, IMediaProjection projection, - IMediaProjectionCallback mediaProjectionCallback, String uniqueId, int uniqueIndex, + IMediaProjectionCallback mediaProjectionCallback, String uniqueId, VirtualDisplayConfig virtualDisplayConfig) { super(VirtualDisplayAdapter.this, displayToken, uniqueId, getContext()); mAppToken = appToken; @@ -306,7 +291,6 @@ public class VirtualDisplayAdapter extends DisplayAdapter { mMediaProjectionCallback = mediaProjectionCallback; mDisplayState = Display.STATE_UNKNOWN; mPendingChanges |= PENDING_SURFACE_CHANGE; - mUniqueIndex = uniqueIndex; mIsDisplayOn = surface != null; mDisplayIdToMirror = virtualDisplayConfig.getDisplayIdToMirror(); mIsWindowManagerMirroring = virtualDisplayConfig.isWindowManagerMirroringEnabled(); diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java index 3f0226663cff..f48178c5b9f7 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java @@ -124,7 +124,7 @@ class WallpaperDisplayHelper { final long ident = Binder.clearCallingIdentity(); try { - return mWindowManagerInternal.shouldShowSystemDecorOnDisplay(displayId); + return mWindowManagerInternal.isHomeSupportedOnDisplay(displayId); } finally { Binder.restoreCallingIdentity(ident); } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 7f80807e137b..49248107a004 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -184,6 +184,7 @@ import android.graphics.Region; import android.graphics.Region.Op; import android.hardware.HardwareBuffer; import android.hardware.display.DisplayManagerInternal; +import android.hardware.display.VirtualDisplayConfig; import android.metrics.LogMaker; import android.os.Bundle; import android.os.Debug; @@ -2191,7 +2192,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp * @see DisplayWindowPolicyController#getCustomHomeComponent() () */ @Nullable ComponentName getCustomHomeComponent() { - if (!supportsSystemDecorations() || mDwpcHelper == null) { + if (!isHomeSupported() || mDwpcHelper == null) { return null; } return mDwpcHelper.getCustomHomeComponent(); @@ -5772,6 +5773,17 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp && isTrusted(); } + /** + * Checks if this display is configured and allowed to show home activity and wallpaper. + * + *

This is implied for displays that have {@link Display#FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS} + * and can also be set via {@link VirtualDisplayConfig.Builder#setHomeSupported}.

+ */ + boolean isHomeSupported() { + return (mWmService.mDisplayWindowSettings.isHomeSupportedLocked(this) && isTrusted()) + || supportsSystemDecorations(); + } + SurfaceControl getWindowingLayer() { return mWindowingLayer; } diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java index e1753d7d6257..7a95c2d6d934 100644 --- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java +++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java @@ -237,6 +237,37 @@ class DisplayWindowSettings { mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings); } + boolean isHomeSupportedLocked(@NonNull DisplayContent dc) { + if (dc.getDisplayId() == Display.DEFAULT_DISPLAY) { + // Default display should show home. + return true; + } + + final DisplayInfo displayInfo = dc.getDisplayInfo(); + final SettingsProvider.SettingsEntry settings = mSettingsProvider.getSettings(displayInfo); + return settings.mIsHomeSupported != null + ? settings.mIsHomeSupported + : shouldShowSystemDecorsLocked(dc); + } + + void setHomeSupportedOnDisplayLocked(@NonNull String displayUniqueId, int displayType, + boolean supported) { + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.uniqueId = displayUniqueId; + displayInfo.type = displayType; + final SettingsProvider.SettingsEntry overrideSettings = + mSettingsProvider.getOverrideSettings(displayInfo); + overrideSettings.mIsHomeSupported = supported; + mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings); + } + + void clearDisplaySettings(@NonNull String displayUniqueId, int displayType) { + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.uniqueId = displayUniqueId; + displayInfo.type = displayType; + mSettingsProvider.clearDisplaySettings(displayInfo); + } + @DisplayImePolicy int getImePolicyLocked(@NonNull DisplayContent dc) { if (dc.getDisplayId() == Display.DEFAULT_DISPLAY) { @@ -382,10 +413,17 @@ class DisplayWindowSettings { void updateOverrideSettings(@NonNull DisplayInfo info, @NonNull SettingsEntry overrides); /** - * Called when a display is removed to cleanup. + * Called when a display is removed to cleanup. Note that for non-virtual displays the + * relevant settings entry will be kept, if non-empty. */ void onDisplayRemoved(@NonNull DisplayInfo info); + /** + * Explicitly removes all settings entory for the given {@link DisplayInfo}, even if it is + * not empty. + */ + void clearDisplaySettings(@NonNull DisplayInfo info); + /** * Settings for a display. */ @@ -411,6 +449,8 @@ class DisplayWindowSettings { @Nullable Boolean mShouldShowSystemDecors; @Nullable + Boolean mIsHomeSupported; + @Nullable Integer mImePolicy; @Nullable Integer mFixedToUserRotation; @@ -479,6 +519,10 @@ class DisplayWindowSettings { mShouldShowSystemDecors = other.mShouldShowSystemDecors; changed = true; } + if (!Objects.equals(other.mIsHomeSupported, mIsHomeSupported)) { + mIsHomeSupported = other.mIsHomeSupported; + changed = true; + } if (!Objects.equals(other.mImePolicy, mImePolicy)) { mImePolicy = other.mImePolicy; changed = true; @@ -561,6 +605,11 @@ class DisplayWindowSettings { mShouldShowSystemDecors = delta.mShouldShowSystemDecors; changed = true; } + if (delta.mIsHomeSupported != null && !Objects.equals( + delta.mIsHomeSupported, mIsHomeSupported)) { + mIsHomeSupported = delta.mIsHomeSupported; + changed = true; + } if (delta.mImePolicy != null && !Objects.equals(delta.mImePolicy, mImePolicy)) { mImePolicy = delta.mImePolicy; @@ -599,6 +648,7 @@ class DisplayWindowSettings { && mRemoveContentMode == REMOVE_CONTENT_MODE_UNDEFINED && mShouldShowWithInsecureKeyguard == null && mShouldShowSystemDecors == null + && mIsHomeSupported == null && mImePolicy == null && mFixedToUserRotation == null && mIgnoreOrientationRequest == null @@ -622,6 +672,7 @@ class DisplayWindowSettings { && Objects.equals(mShouldShowWithInsecureKeyguard, that.mShouldShowWithInsecureKeyguard) && Objects.equals(mShouldShowSystemDecors, that.mShouldShowSystemDecors) + && Objects.equals(mIsHomeSupported, that.mIsHomeSupported) && Objects.equals(mImePolicy, that.mImePolicy) && Objects.equals(mFixedToUserRotation, that.mFixedToUserRotation) && Objects.equals(mIgnoreOrientationRequest, that.mIgnoreOrientationRequest) @@ -633,9 +684,9 @@ class DisplayWindowSettings { public int hashCode() { return Objects.hash(mWindowingMode, mUserRotationMode, mUserRotation, mForcedWidth, mForcedHeight, mForcedDensity, mForcedScalingMode, mRemoveContentMode, - mShouldShowWithInsecureKeyguard, mShouldShowSystemDecors, mImePolicy, - mFixedToUserRotation, mIgnoreOrientationRequest, mIgnoreDisplayCutout, - mDontMoveToTop); + mShouldShowWithInsecureKeyguard, mShouldShowSystemDecors, mIsHomeSupported, + mImePolicy, mFixedToUserRotation, mIgnoreOrientationRequest, + mIgnoreDisplayCutout, mDontMoveToTop); } @Override @@ -651,6 +702,7 @@ class DisplayWindowSettings { + ", mRemoveContentMode=" + mRemoveContentMode + ", mShouldShowWithInsecureKeyguard=" + mShouldShowWithInsecureKeyguard + ", mShouldShowSystemDecors=" + mShouldShowSystemDecors + + ", mIsHomeSupported=" + mIsHomeSupported + ", mShouldShowIme=" + mImePolicy + ", mFixedToUserRotation=" + mFixedToUserRotation + ", mIgnoreOrientationRequest=" + mIgnoreOrientationRequest diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java index ea668faddc37..c79565ae79fa 100644 --- a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java +++ b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java @@ -164,6 +164,11 @@ class DisplayWindowSettingsProvider implements SettingsProvider { mOverrideSettings.onDisplayRemoved(info); } + @Override + public void clearDisplaySettings(@NonNull DisplayInfo info) { + mOverrideSettings.clearDisplaySettings(info); + } + @VisibleForTesting int getOverrideSettingsSize() { return mOverrideSettings.mSettings.size(); @@ -291,6 +296,12 @@ class DisplayWindowSettingsProvider implements SettingsProvider { } } + void clearDisplaySettings(@NonNull DisplayInfo info) { + final String identifier = getIdentifier(info); + mSettings.remove(identifier); + mVirtualDisplayIdentifiers.remove(identifier); + } + private void writeSettings() { final FileData fileData = new FileData(); fileData.mIdentifierType = mIdentifierType; diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 5227a52545f4..fe2c2504abd9 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -1696,8 +1696,8 @@ class RootWindowContainer extends WindowContainer } final DisplayContent display = taskDisplayArea.getDisplayContent(); - if (display == null || display.isRemoved() || !display.supportsSystemDecorations()) { - // Can't launch home on display that doesn't support system decorations. + if (display == null || display.isRemoved() || !display.isHomeSupported()) { + // Can't launch home on display that doesn't support home. return false; } @@ -3126,10 +3126,11 @@ class RootWindowContainer extends WindowContainer if (preferredFocusableRootTask != null) { return preferredFocusableRootTask; } - if (preferredDisplayArea.mDisplayContent.supportsSystemDecorations()) { + + if (preferredDisplayArea.mDisplayContent.isHomeSupported()) { // Stop looking for focusable root task on other displays because the preferred display - // supports system decorations. Home activity would be launched on the same display if - // no focusable root task found. + // supports home. Home activity would be launched on the same display if no focusable + // root task found. return null; } diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index f0a66540061d..c57983c53d37 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -1767,7 +1767,7 @@ final class TaskDisplayArea extends DisplayArea { * Exposes the home task capability of the TaskDisplayArea */ boolean canHostHomeTask() { - return mDisplayContent.supportsSystemDecorations() && mCanHostHomeTask; + return mDisplayContent.isHomeSupported() && mCanHostHomeTask; } /** diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 9f1bccb9a27a..92bd00e0a175 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -28,6 +28,7 @@ import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.Region; import android.hardware.display.DisplayManagerInternal; +import android.hardware.display.VirtualDisplayConfig; import android.os.Bundle; import android.os.IBinder; import android.os.Message; @@ -752,9 +753,31 @@ public abstract class WindowManagerInternal { public abstract Context getTopFocusedDisplayUiContext(); /** - * Checks if this display is configured and allowed to show system decorations. + * Sets whether the relevant display content can host the relevant home activity and wallpaper. + * + * @param displayUniqueId The unique ID of the display. Note that the display may not yet be + * created, but whenever it is, this property will be applied. + * @param displayType The type of the display, e.g. {@link Display#TYPE_VIRTUAL}. + * @param supported Whether home and wallpaper are supported on this display. + */ + public abstract void setHomeSupportedOnDisplay( + @NonNull String displayUniqueId, int displayType, boolean supported); + + /** + * Checks if this display is configured and allowed to show home activity and wallpaper. + * + *

This is implied for displays that have {@link Display#FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS} + * and can also be set via {@link VirtualDisplayConfig.Builder#setHomeSupported}.

+ */ + public abstract boolean isHomeSupportedOnDisplay(int displayId); + + /** + * Removes any settings relevant to the given display. + * + *

This may be used when a property is set for a display unique ID before the display + * creation but the actual display creation failed for some reason.

*/ - public abstract boolean shouldShowSystemDecorOnDisplay(int displayId); + public abstract void clearDisplaySettings(@NonNull String displayUniqueId, int displayType); /** * Indicates the policy for how the display should show IME. diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 757d6d68c3b3..809e2d0dcb85 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -8269,9 +8269,41 @@ public class WindowManagerService extends IWindowManager.Stub } @Override - public boolean shouldShowSystemDecorOnDisplay(int displayId) { + public void setHomeSupportedOnDisplay(String displayUniqueId, int displayType, + boolean supported) { + final long origId = Binder.clearCallingIdentity(); + try { + synchronized (mGlobalLock) { + mDisplayWindowSettings.setHomeSupportedOnDisplayLocked( + displayUniqueId, displayType, supported); + } + } finally { + Binder.restoreCallingIdentity(origId); + } + } + + @Override + public boolean isHomeSupportedOnDisplay(int displayId) { synchronized (mGlobalLock) { - return WindowManagerService.this.shouldShowSystemDecors(displayId); + final DisplayContent displayContent = mRoot.getDisplayContent(displayId); + if (displayContent == null) { + ProtoLog.w(WM_ERROR, "Attempted to get home support flag of a display that " + + "does not exist: %d", displayId); + return false; + } + return displayContent.isHomeSupported(); + } + } + + @Override + public void clearDisplaySettings(String displayUniqueId, int displayType) { + final long origId = Binder.clearCallingIdentity(); + try { + synchronized (mGlobalLock) { + mDisplayWindowSettings.clearDisplaySettings(displayUniqueId, displayType); + } + } finally { + Binder.restoreCallingIdentity(origId); } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index 9684f427adb3..6c9ced80a4e5 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -195,7 +195,8 @@ public class DisplayManagerServiceTest { @Rule(order = 1) public Expect expect = Expect.create(); @Rule - public SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + public SetFlagsRule mSetFlagsRule = + new SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT); private Context mContext; diff --git a/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java index 8bbacc494efd..73e7ba0750e0 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java @@ -77,7 +77,7 @@ public class VirtualDisplayAdapterTest { DisplayDevice result = mVirtualDisplayAdapter.createVirtualDisplayLocked(mMockCallback, /* projection= */ null, /* ownerUid= */ 10, /* packageName= */ "testpackage", - /* surface= */ null, /* flags= */ 0, config); + /* uniqueId= */ "uniqueId", /* surface= */ null, /* flags= */ 0, config); assertNotNull(result); } @@ -89,12 +89,12 @@ public class VirtualDisplayAdapterTest { VirtualDisplayConfig config2 = new VirtualDisplayConfig.Builder("test2", /* width= */ 1, /* height= */ 1, /* densityDpi= */ 1).build(); mVirtualDisplayAdapter.createVirtualDisplayLocked(mMockCallback, /* projection= */ null, - /* ownerUid= */ 10, /* packageName= */ "testpackage", /* surface= */ null, - /* flags= */ 0, config1); + /* ownerUid= */ 10, /* packageName= */ "testpackage", /* uniqueId= */ "uniqueId1", + /* surface= */ null, /* flags= */ 0, config1); DisplayDevice result = mVirtualDisplayAdapter.createVirtualDisplayLocked(mMockCallback, /* projection= */ null, /* ownerUid= */ 10, /* packageName= */ "testpackage", - /* surface= */ null, /* flags= */ 0, config2); + /* uniqueId= */ "uniqueId2", /* surface= */ null, /* flags= */ 0, config2); assertNull(result); } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayAreaPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayAreaPolicyTests.java index 147a44f1d297..fafc03555680 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayAreaPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayAreaPolicyTests.java @@ -190,7 +190,7 @@ public class DisplayAreaPolicyTests extends WindowTestsBase { final WindowManagerService wms = mWm; final DisplayContent displayContent = mock(DisplayContent.class); doReturn(true).when(displayContent).isTrusted(); - doReturn(true).when(displayContent).supportsSystemDecorations(); + doReturn(true).when(displayContent).isHomeSupported(); final RootDisplayArea root = new SurfacelessDisplayAreaRoot(wms); final TaskDisplayArea taskDisplayAreaWithHome = new TaskDisplayArea(displayContent, wms, "Tasks1", FEATURE_DEFAULT_TASK_CONTAINER); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java index e54b8e52a4da..e2524a289b7d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java @@ -496,6 +496,19 @@ public class DisplayWindowSettingsTests extends WindowTestsBase { mPrivateDisplay.getDisplayInfo()); } + @Test + public void testClearDisplaySettings() { + spyOn(mWm.mDisplayWindowSettings); + spyOn(mWm.mDisplayWindowSettingsProvider); + + WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class); + DisplayInfo info = mPrivateDisplay.getDisplayInfo(); + wmInternal.clearDisplaySettings(info.uniqueId, info.type); + + verify(mWm.mDisplayWindowSettings).clearDisplaySettings(info.uniqueId, info.type); + verify(mWm.mDisplayWindowSettingsProvider).clearDisplaySettings(info); + } + public final class TestSettingsProvider implements DisplayWindowSettings.SettingsProvider { Map mOverrideSettingsCache = new HashMap<>(); @@ -530,5 +543,10 @@ public class DisplayWindowSettingsTests extends WindowTestsBase { public void onDisplayRemoved(@NonNull DisplayInfo info) { mOverrideSettingsCache.remove(info); } + + @Override + public void clearDisplaySettings(@NonNull DisplayInfo info) { + mOverrideSettingsCache.remove(info); + } } } -- cgit v1.2.3-59-g8ed1b From c32fa5f33e7d145a745b13a4c9799a08707a3550 Mon Sep 17 00:00:00 2001 From: Ajay Gopi Date: Wed, 15 Nov 2023 22:22:08 +0000 Subject: Add android.permission.RECEIVE_SANDBOX_TRIGGER_AUDIO in Shell manifest. This is required for CtsVoiceInteractionTestCases. Test: presubmit Bug: 291656263 Change-Id: I0fa57a0ec0d337f5bf2970a536e20bdc94d4f312 --- data/etc/privapp-permissions-platform.xml | 1 + packages/Shell/AndroidManifest.xml | 2 ++ 2 files changed, 3 insertions(+) (limited to 'data') diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 3218666771df..1f08955e5f58 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -467,6 +467,7 @@ applications that come with the platform + diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index ed03d94b44c0..b430b24d3940 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -867,6 +867,8 @@ + + Date: Fri, 17 Nov 2023 20:48:00 +0000 Subject: Move LayoutParam secure flag so it's set on the WS level The LayoutParam secure flag set by the client is set on the WSA level. This causes a few issues: 1. Child windows don't inherit this flag since child windows are added beneath WS 2. Prevents moving the WSA's SC to the client since the secure flag needs to be set in WMS. Test: FlagSecureTest Bug: 308662081 Change-Id: I724ab0d834b0d74b33ccbb6bbd2c6f9c622c2a15 --- .../android/window/flags/window_surfaces.aconfig | 8 +++++++ data/etc/services.core.protolog.json | 12 +++++----- .../com/android/server/wm/RootWindowContainer.java | 2 +- .../android/server/wm/WindowManagerService.java | 4 ++-- .../java/com/android/server/wm/WindowState.java | 26 ++++++++++++++++++++++ .../com/android/server/wm/WindowStateAnimator.java | 14 +++++------- .../android/server/wm/WindowSurfaceController.java | 19 ---------------- 7 files changed, 48 insertions(+), 37 deletions(-) (limited to 'data') diff --git a/core/java/android/window/flags/window_surfaces.aconfig b/core/java/android/window/flags/window_surfaces.aconfig index 0da03fb5aaeb..2c9e1f2a9bcc 100644 --- a/core/java/android/window/flags/window_surfaces.aconfig +++ b/core/java/android/window/flags/window_surfaces.aconfig @@ -48,3 +48,11 @@ flag { is_fixed_read_only: true bug: "262477923" } + +flag { + namespace: "window_surfaces" + name: "secure_window_state" + description: "Move SC secure flag to WindowState level" + is_fixed_read_only: true + bug: "308662081" +} diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index f19acbe023b8..d36ac3914ff3 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -991,12 +991,6 @@ "group": "WM_DEBUG_WINDOW_INSETS", "at": "com\/android\/server\/wm\/InsetsSourceProvider.java" }, - "-1176488860": { - "message": "SURFACE isSecure=%b: %s", - "level": "INFO", - "group": "WM_SHOW_TRANSACTIONS", - "at": "com\/android\/server\/wm\/WindowSurfaceController.java" - }, "-1164930508": { "message": "Moving to RESUMED: %s (starting new instance) callers=%s", "level": "VERBOSE", @@ -3277,6 +3271,12 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "810599500": { + "message": "SURFACE isSecure=%b: %s", + "level": "INFO", + "group": "WM_SHOW_TRANSACTIONS", + "at": "com\/android\/server\/wm\/WindowState.java" + }, "829434921": { "message": "Draw state now committed in %s", "level": "VERBOSE", diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 0c235bae2006..7f5f178b4ace 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -627,7 +627,7 @@ class RootWindowContainer extends WindowContainer void refreshSecureSurfaceState() { forAllWindows((w) -> { if (w.mHasSurface) { - w.mWinAnimator.setSecureLocked(w.isSecureLocked()); + w.setSecureLocked(w.isSecureLocked()); } }, true /* traverseTopToBottom */); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index a69a07f9aee0..bf33742ed8bb 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -2327,8 +2327,8 @@ public class WindowManagerService extends IWindowManager.Stub boolean wallpaperMayMove = win.mViewVisibility != viewVisibility && win.hasWallpaper(); wallpaperMayMove |= (flagChanges & FLAG_SHOW_WALLPAPER) != 0; - if ((flagChanges & FLAG_SECURE) != 0 && winAnimator.mSurfaceController != null) { - winAnimator.mSurfaceController.setSecure(win.isSecureLocked()); + if ((flagChanges & FLAG_SECURE) != 0) { + win.setSecureLocked(win.isSecureLocked()); } final boolean wasVisible = win.isVisible(); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 3e43908994ad..4103d980f75c 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -109,6 +109,7 @@ import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_RESIZE; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STARTING_WINDOW; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_SYNC_ENGINE; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_INSETS; +import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_TRANSACTIONS; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER; import static com.android.server.policy.WindowManagerPolicy.TRANSIT_ENTER; import static com.android.server.policy.WindowManagerPolicy.TRANSIT_EXIT; @@ -177,6 +178,7 @@ import static com.android.server.wm.WindowStateProto.UNRESTRICTED_KEEP_CLEAR_ARE import static com.android.server.wm.WindowStateProto.VIEW_VISIBILITY; import static com.android.server.wm.WindowStateProto.WINDOW_CONTAINER; import static com.android.server.wm.WindowStateProto.WINDOW_FRAMES; +import static com.android.window.flags.Flags.secureWindowState; import static com.android.window.flags.Flags.surfaceTrustedOverlay; import android.annotation.CallSuper; @@ -1195,6 +1197,9 @@ class WindowState extends WindowContainer implements WindowManagerP if (surfaceTrustedOverlay() && isWindowTrustedOverlay()) { getPendingTransaction().setTrustedOverlay(mSurfaceControl, true); } + if (secureWindowState()) { + getPendingTransaction().setSecure(mSurfaceControl, isSecureLocked()); + } } void updateTrustedOverlay() { @@ -6040,4 +6045,25 @@ class WindowState extends WindowContainer implements WindowManagerP // Cancel any draw requests during a sync. return mPrepareSyncSeqId > 0; } + + void setSecureLocked(boolean isSecure) { + ProtoLog.i(WM_SHOW_TRANSACTIONS, "SURFACE isSecure=%b: %s", isSecure, getName()); + if (secureWindowState()) { + if (mSurfaceControl == null) { + return; + } + getPendingTransaction().setSecure(mSurfaceControl, isSecure); + } else { + if (mWinAnimator.mSurfaceController == null + || mWinAnimator.mSurfaceController.mSurfaceControl == null) { + return; + } + getPendingTransaction().setSecure(mWinAnimator.mSurfaceController.mSurfaceControl, + isSecure); + } + if (mDisplayContent != null) { + mDisplayContent.refreshImeSecureFlag(getSyncTransaction()); + } + mWmService.scheduleAnimationLocked(); + } } diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java index 3aac816fcd7a..44cd23d037c6 100644 --- a/services/core/java/com/android/server/wm/WindowStateAnimator.java +++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java @@ -44,6 +44,7 @@ import static com.android.server.wm.WindowManagerService.logWithStack; import static com.android.server.wm.WindowStateAnimatorProto.DRAW_STATE; import static com.android.server.wm.WindowStateAnimatorProto.SURFACE; import static com.android.server.wm.WindowStateAnimatorProto.SYSTEM_DECOR_RECT; +import static com.android.window.flags.Flags.secureWindowState; import android.content.Context; import android.graphics.PixelFormat; @@ -286,8 +287,10 @@ class WindowStateAnimator { int flags = SurfaceControl.HIDDEN; final WindowManager.LayoutParams attrs = w.mAttrs; - if (w.isSecureLocked()) { - flags |= SurfaceControl.SECURE; + if (!secureWindowState()) { + if (w.isSecureLocked()) { + flags |= SurfaceControl.SECURE; + } } if ((mWin.mAttrs.privateFlags & PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY) != 0) { @@ -488,13 +491,6 @@ class WindowStateAnimator { mSurfaceController.setOpaque(isOpaque); } - void setSecureLocked(boolean isSecure) { - if (mSurfaceController == null) { - return; - } - mSurfaceController.setSecure(isSecure); - } - void setColorSpaceAgnosticLocked(boolean agnostic) { if (mSurfaceController == null) { return; diff --git a/services/core/java/com/android/server/wm/WindowSurfaceController.java b/services/core/java/com/android/server/wm/WindowSurfaceController.java index d348491b3d2a..4456a94ef510 100644 --- a/services/core/java/com/android/server/wm/WindowSurfaceController.java +++ b/services/core/java/com/android/server/wm/WindowSurfaceController.java @@ -24,7 +24,6 @@ import static android.view.SurfaceControl.METADATA_WINDOW_TYPE; import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_SURFACE_ALLOC; import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_TRANSACTIONS; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_VISIBILITY; -import static com.android.server.wm.WindowManagerDebugConfig.SHOW_LIGHT_TRANSACTIONS; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import static com.android.server.wm.WindowSurfaceControllerProto.SHOWN; @@ -152,24 +151,6 @@ class WindowSurfaceController { mService.scheduleAnimationLocked(); } - void setSecure(boolean isSecure) { - ProtoLog.i(WM_SHOW_TRANSACTIONS, "SURFACE isSecure=%b: %s", isSecure, title); - - if (mSurfaceControl == null) { - return; - } - if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG, ">>> OPEN TRANSACTION setSecureLocked"); - - final SurfaceControl.Transaction t = mAnimator.mWin.getPendingTransaction(); - t.setSecure(mSurfaceControl, isSecure); - - final DisplayContent dc = mAnimator.mWin.mDisplayContent; - if (dc != null) { - dc.refreshImeSecureFlag(t); - } - mService.scheduleAnimationLocked(); - } - void setColorSpaceAgnostic(SurfaceControl.Transaction t, boolean agnostic) { ProtoLog.i(WM_SHOW_TRANSACTIONS, "SURFACE isColorSpaceAgnostic=%b: %s", agnostic, title); -- cgit v1.2.3-59-g8ed1b From 5496dfa0eb103f7954df94aea257ce482bde184d Mon Sep 17 00:00:00 2001 From: Marzia Favaro Date: Fri, 3 Nov 2023 17:32:18 +0000 Subject: Improve readability and add protections against use after release NPE Preventively ensure the dimLayer has not been released before trying to access it. Fix: 308448047 Test: atest CtsWindowManagerDeviceOther:android.server.wm.other.MinimalPostProcessingTests#testTwoVisibleWindowsSecondOnePrefersMinimalPostProcessing --iteration 100 Test: atest DimmerTests Change-Id: Id08838e0a98ba949382442b8fac8e81156d1aa40 --- data/etc/services.core.protolog.json | 42 +- .../core/java/com/android/server/wm/Dimmer.java | 4 +- .../android/server/wm/DimmerAnimationHelper.java | 334 ++++++++++++++++ .../java/com/android/server/wm/SmoothDimmer.java | 423 ++++++--------------- .../src/com/android/server/wm/DimmerTests.java | 3 +- 5 files changed, 475 insertions(+), 331 deletions(-) create mode 100644 services/core/java/com/android/server/wm/DimmerAnimationHelper.java (limited to 'data') diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index dc2b0561957d..9d76c9f5fc93 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -535,6 +535,12 @@ "group": "WM_DEBUG_TASKS", "at": "com\/android\/server\/wm\/ActivityStarter.java" }, + "-1582845629": { + "message": "Starting animation on %s", + "level": "VERBOSE", + "group": "WM_DEBUG_DIMMER", + "at": "com\/android\/server\/wm\/DimmerAnimationHelper.java" + }, "-1575977269": { "message": "Skipping %s: mismatch root %s", "level": "DEBUG", @@ -925,6 +931,12 @@ "group": "WM_DEBUG_REMOTE_ANIMATIONS", "at": "com\/android\/server\/wm\/RemoteAnimationController.java" }, + "-1243510456": { + "message": "Dim animation requested: %s", + "level": "VERBOSE", + "group": "WM_DEBUG_DIMMER", + "at": "com\/android\/server\/wm\/DimmerAnimationHelper.java" + }, "-1237827119": { "message": "Schedule remove starting %s startingWindow=%s animate=%b Callers=%s", "level": "VERBOSE", @@ -1177,6 +1189,12 @@ "group": "WM_DEBUG_BACK_PREVIEW", "at": "com\/android\/server\/wm\/BackNavigationController.java" }, + "-1028213464": { + "message": "%s skipping animation and directly setting alpha=%f, blur=%d", + "level": "DEBUG", + "group": "WM_DEBUG_DIMMER", + "at": "com\/android\/server\/wm\/DimmerAnimationHelper.java" + }, "-1022146708": { "message": "Skipping %s: mismatch activity type", "level": "DEBUG", @@ -1807,12 +1825,6 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, - "-504637678": { - "message": "Starting animation on dim layer %s, requested by %s, alpha: %f -> %f, blur: %d -> %d", - "level": "VERBOSE", - "group": "WM_DEBUG_DIMMER", - "at": "com\/android\/server\/wm\/SmoothDimmer.java" - }, "-503656156": { "message": "Update process config of %s to new config %s", "level": "VERBOSE", @@ -2731,12 +2743,6 @@ "group": "WM_DEBUG_STATES", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, - "309039362": { - "message": "SURFACE MATRIX [%f,%f,%f,%f]: %s", - "level": "INFO", - "group": "WM_SHOW_TRANSACTIONS", - "at": "com\/android\/server\/wm\/WindowSurfaceController.java" - }, "312030608": { "message": "New topFocusedDisplayId=%d", "level": "DEBUG", @@ -3091,12 +3097,6 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, - "633654009": { - "message": "SURFACE POS (setPositionInTransaction) @ (%f,%f): %s", - "level": "INFO", - "group": "WM_SHOW_TRANSACTIONS", - "at": "com\/android\/server\/wm\/WindowSurfaceController.java" - }, "638429464": { "message": "\tRemove container=%s", "level": "DEBUG", @@ -4039,12 +4039,6 @@ "group": "WM_DEBUG_STATES", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, - "1620751818": { - "message": "Dim %s skipping animation and directly setting alpha=%f, blur=%d", - "level": "DEBUG", - "group": "WM_DEBUG_DIMMER", - "at": "com\/android\/server\/wm\/SmoothDimmer.java" - }, "1621562070": { "message": " startWCT=%s", "level": "VERBOSE", diff --git a/services/core/java/com/android/server/wm/Dimmer.java b/services/core/java/com/android/server/wm/Dimmer.java index 2fabb0ea686a..7ce9de4e1c24 100644 --- a/services/core/java/com/android/server/wm/Dimmer.java +++ b/services/core/java/com/android/server/wm/Dimmer.java @@ -83,6 +83,7 @@ public abstract class Dimmer { /** * Mark all dims as pending completion on the next call to {@link #updateDims} * + * Called before iterating on mHost's children, first step of dimming. * This is intended for us by the host container, to be called at the beginning of * {@link WindowContainer#prepareSurfaces}. After calling this, the container should * chain {@link WindowContainer#prepareSurfaces} down to it's children to give them @@ -100,8 +101,7 @@ public abstract class Dimmer { /** * Call after invoking {@link WindowContainer#prepareSurfaces} on children as - * described in {@link #resetDimStates}. The dim bounds returned by {@link #resetDimStates} - * should be set before calling this method. + * described in {@link #resetDimStates}. * * @param t A transaction in which to update the dims. * @return true if any Dims were updated. diff --git a/services/core/java/com/android/server/wm/DimmerAnimationHelper.java b/services/core/java/com/android/server/wm/DimmerAnimationHelper.java new file mode 100644 index 000000000000..e91857f1da82 --- /dev/null +++ b/services/core/java/com/android/server/wm/DimmerAnimationHelper.java @@ -0,0 +1,334 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_DIMMER; +import static com.android.server.wm.AlphaAnimationSpecProto.DURATION_MS; +import static com.android.server.wm.AlphaAnimationSpecProto.FROM; +import static com.android.server.wm.AlphaAnimationSpecProto.TO; +import static com.android.server.wm.AnimationSpecProto.ALPHA; +import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_DIMMER; +import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; + +import android.util.Log; +import android.util.proto.ProtoOutputStream; +import android.view.SurfaceControl; + +import com.android.internal.protolog.common.ProtoLog; + +import java.io.PrintWriter; + +/** + * Contains the information relative to the changes to apply to the dim layer + */ +public class DimmerAnimationHelper { + private static final String TAG = TAG_WITH_CLASS_NAME ? "DimmerAnimationHelper" : TAG_WM; + private static final int DEFAULT_DIM_ANIM_DURATION_MS = 200; + + /** + * Contains the requested changes + */ + static class Change { + private float mAlpha = -1f; + private int mBlurRadius = -1; + private WindowContainer mDimmingContainer = null; + private int mRelativeLayer = -1; + private static final float EPSILON = 0.0001f; + + Change() {} + + Change(Change other) { + mAlpha = other.mAlpha; + mBlurRadius = other.mBlurRadius; + mDimmingContainer = other.mDimmingContainer; + mRelativeLayer = other.mRelativeLayer; + } + + // Same alpha and blur + boolean hasSameVisualProperties(Change other) { + return Math.abs(mAlpha - other.mAlpha) < EPSILON && mBlurRadius == other.mBlurRadius; + } + + boolean hasSameDimmingContainer(Change other) { + return mDimmingContainer != null && mDimmingContainer == other.mDimmingContainer; + } + + void inheritPropertiesFromAnimation(AnimationSpec anim) { + mAlpha = anim.mCurrentAlpha; + mBlurRadius = anim.mCurrentBlur; + } + + @Override + public String toString() { + return "Dim state: alpha=" + mAlpha + ", blur=" + mBlurRadius + ", container=" + + mDimmingContainer + ", relativePosition=" + mRelativeLayer; + } + } + + private Change mCurrentProperties = new Change(); + private Change mRequestedProperties = new Change(); + private AnimationSpec mAlphaAnimationSpec; + + private final AnimationAdapterFactory mAnimationAdapterFactory; + private AnimationAdapter mLocalAnimationAdapter; + + DimmerAnimationHelper(AnimationAdapterFactory animationFactory) { + mAnimationAdapterFactory = animationFactory; + } + + void setExitParameters() { + setRequestedRelativeParent(mRequestedProperties.mDimmingContainer, -1 /* relativeLayer */); + setRequestedAppearance(0f /* alpha */, 0 /* blur */); + } + + // Sets a requested change without applying it immediately + void setRequestedRelativeParent(WindowContainer relativeParent, int relativeLayer) { + mRequestedProperties.mDimmingContainer = relativeParent; + mRequestedProperties.mRelativeLayer = relativeLayer; + } + + // Sets a requested change without applying it immediately + void setRequestedAppearance(float alpha, int blurRadius) { + mRequestedProperties.mAlpha = alpha; + mRequestedProperties.mBlurRadius = blurRadius; + } + + /** + * Commit the last changes we received. Called after + * {@link Change#setExitParameters()}, + * {@link Change#setRequestedRelativeParent(WindowContainer, int)}, or + * {@link Change#setRequestedAppearance(float, int)} + */ + void applyChanges(SurfaceControl.Transaction t, SmoothDimmer.DimState dim) { + if (mRequestedProperties.mDimmingContainer == null) { + Log.e(TAG, this + " does not have a dimming container. Have you forgotten to " + + "call adjustRelativeLayer?"); + return; + } + if (mRequestedProperties.mDimmingContainer.mSurfaceControl == null) { + Log.w(TAG, "container " + mRequestedProperties.mDimmingContainer + + "does not have a surface"); + dim.remove(t); + return; + } + + dim.ensureVisible(t); + relativeReparent(dim.mDimSurface, + mRequestedProperties.mDimmingContainer.getSurfaceControl(), + mRequestedProperties.mRelativeLayer, t); + + if (!mCurrentProperties.hasSameVisualProperties(mRequestedProperties)) { + stopCurrentAnimation(dim.mDimSurface); + + if (dim.mSkipAnimation + // If the container doesn't change but requests a dim change, then it is + // directly providing us the animated values + || (mRequestedProperties.hasSameDimmingContainer(mCurrentProperties) + && dim.isDimming())) { + ProtoLog.d(WM_DEBUG_DIMMER, + "%s skipping animation and directly setting alpha=%f, blur=%d", + dim, mRequestedProperties.mAlpha, + mRequestedProperties.mBlurRadius); + setAlphaBlur(dim.mDimSurface, mRequestedProperties.mAlpha, + mRequestedProperties.mBlurRadius, t); + dim.mSkipAnimation = false; + } else { + startAnimation(t, dim); + } + + } else if (!dim.isDimming()) { + // We are not dimming, so we tried the exit animation but the alpha is already 0, + // therefore, let's just remove this surface + dim.remove(t); + } + mCurrentProperties = new Change(mRequestedProperties); + } + + private void startAnimation( + SurfaceControl.Transaction t, SmoothDimmer.DimState dim) { + ProtoLog.v(WM_DEBUG_DIMMER, "Starting animation on %s", dim); + mAlphaAnimationSpec = getRequestedAnimationSpec(); + mLocalAnimationAdapter = mAnimationAdapterFactory.get(mAlphaAnimationSpec, + dim.mHostContainer.mWmService.mSurfaceAnimationRunner); + + float targetAlpha = mRequestedProperties.mAlpha; + int targetBlur = mRequestedProperties.mBlurRadius; + + mLocalAnimationAdapter.startAnimation(dim.mDimSurface, t, + ANIMATION_TYPE_DIMMER, /* finishCallback */ (type, animator) -> { + setAlphaBlur(dim.mDimSurface, targetAlpha, targetBlur, t); + if (targetAlpha == 0f && !dim.isDimming()) { + dim.remove(t); + } + mLocalAnimationAdapter = null; + mAlphaAnimationSpec = null; + }); + } + + private boolean isAnimating() { + return mAlphaAnimationSpec != null; + } + + void stopCurrentAnimation(SurfaceControl surface) { + if (mLocalAnimationAdapter != null && isAnimating()) { + // Save the current animation progress and cancel the animation + mCurrentProperties.inheritPropertiesFromAnimation(mAlphaAnimationSpec); + mLocalAnimationAdapter.onAnimationCancelled(surface); + mLocalAnimationAdapter = null; + mAlphaAnimationSpec = null; + } + } + + private AnimationSpec getRequestedAnimationSpec() { + final float startAlpha = Math.max(mCurrentProperties.mAlpha, 0f); + final int startBlur = Math.max(mCurrentProperties.mBlurRadius, 0); + long duration = (long) (getDimDuration(mRequestedProperties.mDimmingContainer) + * Math.abs(mRequestedProperties.mAlpha - startAlpha)); + + final AnimationSpec spec = new AnimationSpec( + new AnimationSpec.AnimationExtremes<>(startAlpha, mRequestedProperties.mAlpha), + new AnimationSpec.AnimationExtremes<>(startBlur, mRequestedProperties.mBlurRadius), + duration + ); + ProtoLog.v(WM_DEBUG_DIMMER, "Dim animation requested: %s", spec); + return spec; + } + + /** + * Change the relative parent of this dim layer + */ + void relativeReparent(SurfaceControl dimLayer, SurfaceControl relativeParent, + int relativePosition, SurfaceControl.Transaction t) { + try { + t.setRelativeLayer(dimLayer, relativeParent, relativePosition); + } catch (NullPointerException e) { + Log.w(TAG, "Tried to change parent of dim " + dimLayer + " after remove", e); + } + } + + void setAlphaBlur(SurfaceControl sc, float alpha, int blur, SurfaceControl.Transaction t) { + try { + t.setAlpha(sc, alpha); + t.setBackgroundBlurRadius(sc, blur); + } catch (NullPointerException e) { + Log.w(TAG , "Tried to change look of dim " + sc + " after remove", e); + } + } + + private long getDimDuration(WindowContainer container) { + // Use the same duration as the animation on the WindowContainer + AnimationAdapter animationAdapter = container.mSurfaceAnimator.getAnimation(); + final float durationScale = container.mWmService.getTransitionAnimationScaleLocked(); + return animationAdapter == null ? (long) (DEFAULT_DIM_ANIM_DURATION_MS * durationScale) + : animationAdapter.getDurationHint(); + } + + /** + * Collects the animation specifics + */ + static class AnimationSpec implements LocalAnimationAdapter.AnimationSpec { + private static final String TAG = TAG_WITH_CLASS_NAME ? "DimmerAnimationSpec" : TAG_WM; + + static class AnimationExtremes { + final T mStartValue; + final T mFinishValue; + + AnimationExtremes(T fromValue, T toValue) { + mStartValue = fromValue; + mFinishValue = toValue; + } + + @Override + public String toString() { + return "[" + mStartValue + "->" + mFinishValue + "]"; + } + } + + private final long mDuration; + private final AnimationSpec.AnimationExtremes mAlpha; + private final AnimationSpec.AnimationExtremes mBlur; + + float mCurrentAlpha = 0; + int mCurrentBlur = 0; + boolean mStarted = false; + + AnimationSpec(AnimationSpec.AnimationExtremes alpha, + AnimationSpec.AnimationExtremes blur, long duration) { + mAlpha = alpha; + mBlur = blur; + mDuration = duration; + } + + @Override + public long getDuration() { + return mDuration; + } + + @Override + public void apply(SurfaceControl.Transaction t, SurfaceControl sc, long currentPlayTime) { + if (!mStarted) { + // The first frame would end up in the sync transaction, and since this could be + // applied after the animation transaction, we avoid putting visible changes here. + // The initial state of the animation matches the current state of the dim anyway. + mStarted = true; + return; + } + final float fraction = getFraction(currentPlayTime); + mCurrentAlpha = + fraction * (mAlpha.mFinishValue - mAlpha.mStartValue) + mAlpha.mStartValue; + mCurrentBlur = + (int) fraction * (mBlur.mFinishValue - mBlur.mStartValue) + mBlur.mStartValue; + if (sc.isValid()) { + t.setAlpha(sc, mCurrentAlpha); + t.setBackgroundBlurRadius(sc, mCurrentBlur); + } else { + Log.w(TAG, "Dimmer#AnimationSpec tried to access " + sc + " after release"); + } + } + + @Override + public String toString() { + return "Animation spec: alpha=" + mAlpha + ", blur=" + mBlur; + } + + @Override + public void dump(PrintWriter pw, String prefix) { + pw.print(prefix); pw.print("from_alpha="); pw.print(mAlpha.mStartValue); + pw.print(" to_alpha="); pw.print(mAlpha.mFinishValue); + pw.print(prefix); pw.print("from_blur="); pw.print(mBlur.mStartValue); + pw.print(" to_blur="); pw.print(mBlur.mFinishValue); + pw.print(" duration="); pw.println(mDuration); + } + + @Override + public void dumpDebugInner(ProtoOutputStream proto) { + final long token = proto.start(ALPHA); + proto.write(FROM, mAlpha.mStartValue); + proto.write(TO, mAlpha.mFinishValue); + proto.write(DURATION_MS, mDuration); + proto.end(token); + } + } + + static class AnimationAdapterFactory { + public AnimationAdapter get(LocalAnimationAdapter.AnimationSpec alphaAnimationSpec, + SurfaceAnimationRunner runner) { + return new LocalAnimationAdapter(alphaAnimationSpec, runner); + } + } +} diff --git a/services/core/java/com/android/server/wm/SmoothDimmer.java b/services/core/java/com/android/server/wm/SmoothDimmer.java index 2549bbf70e9f..b5d94a2efbdf 100644 --- a/services/core/java/com/android/server/wm/SmoothDimmer.java +++ b/services/core/java/com/android/server/wm/SmoothDimmer.java @@ -17,397 +17,212 @@ package com.android.server.wm; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_DIMMER; -import static com.android.server.wm.AlphaAnimationSpecProto.DURATION_MS; -import static com.android.server.wm.AlphaAnimationSpecProto.FROM; -import static com.android.server.wm.AlphaAnimationSpecProto.TO; -import static com.android.server.wm.AnimationSpecProto.ALPHA; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_DIMMER; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import android.graphics.Rect; import android.util.Log; -import android.util.proto.ProtoOutputStream; import android.view.Surface; import android.view.SurfaceControl; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; -import java.io.PrintWriter; - class SmoothDimmer extends Dimmer { + private static final String TAG = TAG_WITH_CLASS_NAME ? "Dimmer" : TAG_WM; - private static final float EPSILON = 0.0001f; - // This is in milliseconds. - private static final int DEFAULT_DIM_ANIM_DURATION = 200; DimState mDimState; - private WindowContainer mLastRequestedDimContainer; - private final AnimationAdapterFactory mAnimationAdapterFactory; + final DimmerAnimationHelper.AnimationAdapterFactory mAnimationAdapterFactory; + /** + * Controls the dim behaviour + */ @VisibleForTesting class DimState { - /** - * The layer where property changes should be invoked on. - */ - SurfaceControl mDimLayer; - boolean mDimming; - boolean mIsVisible; - + /** Related objects */ + SurfaceControl mDimSurface; + final WindowContainer mHostContainer; + // The last container to request to dim + private WindowContainer mLastRequestedDimContainer; + /** Animation */ + private final DimmerAnimationHelper mAnimationHelper; + boolean mSkipAnimation = false; + // Determines whether the dim layer should animate before destroying. + boolean mAnimateExit = true; + /** Surface visibility and bounds */ + private boolean mIsVisible = false; // TODO(b/64816140): Remove after confirming dimmer layer always matches its container. final Rect mDimBounds = new Rect(); - /** - * Determines whether the dim layer should animate before destroying. - */ - boolean mAnimateExit = true; - - /** - * Used for Dims not associated with a WindowContainer. - * See {@link Dimmer#adjustRelativeLayer(WindowContainer, int)} for details on Dim - * lifecycle. - */ - boolean mDontReset; - - Change mCurrentProperties; - Change mRequestedProperties; - private AnimationSpec mAlphaAnimationSpec; - private AnimationAdapter mLocalAnimationAdapter; - - static class Change { - private float mAlpha = -1f; - private int mBlurRadius = -1; - private WindowContainer mDimmingContainer = null; - private int mRelativeLayer = -1; - private boolean mSkipAnimation = false; - - Change() {} - - Change(Change other) { - mAlpha = other.mAlpha; - mBlurRadius = other.mBlurRadius; - mDimmingContainer = other.mDimmingContainer; - mRelativeLayer = other.mRelativeLayer; - } - - @Override - public String toString() { - return "Dim state: alpha=" + mAlpha + ", blur=" + mBlurRadius + ", container=" - + mDimmingContainer + ", relativePosition=" + mRelativeLayer - + ", skipAnimation=" + mSkipAnimation; + DimState() { + mHostContainer = mHost; + mAnimationHelper = new DimmerAnimationHelper(mAnimationAdapterFactory); + try { + mDimSurface = makeDimLayer(); + } catch (Surface.OutOfResourcesException e) { + Log.w(TAG, "OutOfResourcesException creating dim surface"); } } - DimState(SurfaceControl dimLayer) { - mDimLayer = dimLayer; - mDimming = true; - mCurrentProperties = new Change(); - mRequestedProperties = new Change(); - } - - void setExitParameters(WindowContainer container) { - setRequestedRelativeParent(container, -1 /* relativeLayer */); - setRequestedAppearance(0f /* alpha */, 0 /* blur */); + void ensureVisible(SurfaceControl.Transaction t) { + if (!mIsVisible) { + t.show(mDimSurface); + t.setAlpha(mDimSurface, 0f); + mIsVisible = true; + } } - // Sets a requested change without applying it immediately - void setRequestedRelativeParent(WindowContainer relativeParent, int relativeLayer) { - mRequestedProperties.mDimmingContainer = relativeParent; - mRequestedProperties.mRelativeLayer = relativeLayer; + void adjustSurfaceLayout(SurfaceControl.Transaction t) { + // TODO: Once we use geometry from hierarchy this falls away. + t.setPosition(mDimSurface, mDimBounds.left, mDimBounds.top); + t.setWindowCrop(mDimSurface, mDimBounds.width(), mDimBounds.height()); } - // Sets a requested change without applying it immediately - void setRequestedAppearance(float alpha, int blurRadius) { - mRequestedProperties.mAlpha = alpha; - mRequestedProperties.mBlurRadius = blurRadius; + /** + * Set the parameters to prepare the dim to change its appearance + */ + void prepareLookChange(float alpha, int blurRadius) { + mAnimationHelper.setRequestedAppearance(alpha, blurRadius); } /** - * Commit the last changes we received. Called after - * {@link Change#setExitParameters(WindowContainer)}, - * {@link Change#setRequestedRelativeParent(WindowContainer, int)}, or - * {@link Change#setRequestedAppearance(float, int)} + * Prepare the dim for the exit animation */ - void applyChanges(SurfaceControl.Transaction t) { - if (mRequestedProperties.mDimmingContainer == null) { - Log.e(TAG, this + " does not have a dimming container. Have you forgotten to " - + "call adjustRelativeLayer?"); - return; - } - if (mRequestedProperties.mDimmingContainer.mSurfaceControl == null) { - Log.w(TAG, "container " + mRequestedProperties.mDimmingContainer - + "does not have a surface"); - return; - } - if (!mDimState.mIsVisible) { - mDimState.mIsVisible = true; - t.show(mDimState.mDimLayer); + void exit(SurfaceControl.Transaction t) { + if (!mAnimateExit) { + remove(t); + } else { + mAnimationHelper.setExitParameters(); + setReady(t); } - t.setRelativeLayer(mDimLayer, - mRequestedProperties.mDimmingContainer.getSurfaceControl(), - mRequestedProperties.mRelativeLayer); + } - if (aspectChanged()) { - if (isAnimating()) { - mLocalAnimationAdapter.onAnimationCancelled(mDimLayer); - } - if (mRequestedProperties.mSkipAnimation - || (!dimmingContainerChanged() && mDimming)) { - // If the dimming container has not changed, then it is running its own - // animation, thus we can directly set the values we get requested, unless it's - // the exiting animation - ProtoLog.d(WM_DEBUG_DIMMER, - "Dim %s skipping animation and directly setting alpha=%f, blur=%d", - mDimLayer, mRequestedProperties.mAlpha, - mRequestedProperties.mBlurRadius); - t.setAlpha(mDimLayer, mRequestedProperties.mAlpha); - t.setBackgroundBlurRadius(mDimLayer, mRequestedProperties.mBlurRadius); - mRequestedProperties.mSkipAnimation = false; - } else { - startAnimation(t); - } + void remove(SurfaceControl.Transaction t) { + mAnimationHelper.stopCurrentAnimation(mDimSurface); + if (mDimSurface.isValid()) { + t.remove(mDimSurface); + ProtoLog.d(WM_DEBUG_DIMMER, + "Removing dim surface %s on transaction %s", this, t); + } else { + Log.w(TAG, "Tried to remove " + mDimSurface + " multiple times\n"); } - mCurrentProperties = new Change(mRequestedProperties); } - private void startAnimation(SurfaceControl.Transaction t) { - mAlphaAnimationSpec = getRequestedAnimationSpec(mRequestedProperties.mAlpha, - mRequestedProperties.mBlurRadius); - mLocalAnimationAdapter = mAnimationAdapterFactory.get(mAlphaAnimationSpec, - mHost.mWmService.mSurfaceAnimationRunner); - - mLocalAnimationAdapter.startAnimation(mDimLayer, t, - ANIMATION_TYPE_DIMMER, (type, animator) -> { - t.setAlpha(mDimLayer, mRequestedProperties.mAlpha); - t.setBackgroundBlurRadius(mDimLayer, mRequestedProperties.mBlurRadius); - if (mRequestedProperties.mAlpha == 0f && !mDimming) { - ProtoLog.d(WM_DEBUG_DIMMER, - "Removing dim surface %s on transaction %s", mDimLayer, t); - t.remove(mDimLayer); - } - mLocalAnimationAdapter = null; - mAlphaAnimationSpec = null; - }); + @Override + public String toString() { + return "SmoothDimmer#DimState with host=" + mHostContainer + ", surface=" + mDimSurface; } - private boolean isAnimating() { - return mAlphaAnimationSpec != null; + /** + * Set the parameters to prepare the dim to be relative parented to the dimming container + */ + void prepareReparent(WindowContainer relativeParent, int relativeLayer) { + mAnimationHelper.setRequestedRelativeParent(relativeParent, relativeLayer); } - private boolean aspectChanged() { - return Math.abs(mRequestedProperties.mAlpha - mCurrentProperties.mAlpha) > EPSILON - || mRequestedProperties.mBlurRadius != mCurrentProperties.mBlurRadius; + /** + * Call when all the changes have been requested to have them applied + * @param t The transaction in which to apply the changes + */ + void setReady(SurfaceControl.Transaction t) { + mAnimationHelper.applyChanges(t, this); } - private boolean dimmingContainerChanged() { - return mRequestedProperties.mDimmingContainer != mCurrentProperties.mDimmingContainer; + /** + * Whether anyone is currently requesting the dim + */ + boolean isDimming() { + return mLastRequestedDimContainer != null; } - private AnimationSpec getRequestedAnimationSpec(float targetAlpha, int targetBlur) { - final float startAlpha; - final int startBlur; - if (mAlphaAnimationSpec != null) { - startAlpha = mAlphaAnimationSpec.mCurrentAlpha; - startBlur = mAlphaAnimationSpec.mCurrentBlur; - } else { - startAlpha = Math.max(mCurrentProperties.mAlpha, 0f); - startBlur = Math.max(mCurrentProperties.mBlurRadius, 0); - } - long duration = (long) (getDimDuration(mRequestedProperties.mDimmingContainer) - * Math.abs(targetAlpha - startAlpha)); - - ProtoLog.v(WM_DEBUG_DIMMER, "Starting animation on dim layer %s, requested by %s, " - + "alpha: %f -> %f, blur: %d -> %d", - mDimLayer, mRequestedProperties.mDimmingContainer, startAlpha, targetAlpha, - startBlur, targetBlur); - return new AnimationSpec( - new AnimationExtremes<>(startAlpha, targetAlpha), - new AnimationExtremes<>(startBlur, targetBlur), - duration - ); + private SurfaceControl makeDimLayer() { + return mHost.makeChildSurface(null) + .setParent(mHost.getSurfaceControl()) + .setColorLayer() + .setName("Dim Layer for - " + mHost.getName()) + .setCallsite("DimLayer.makeDimLayer") + .build(); } } protected SmoothDimmer(WindowContainer host) { - this(host, new AnimationAdapterFactory()); + this(host, new DimmerAnimationHelper.AnimationAdapterFactory()); } @VisibleForTesting - SmoothDimmer(WindowContainer host, AnimationAdapterFactory animationFactory) { + SmoothDimmer(WindowContainer host, + DimmerAnimationHelper.AnimationAdapterFactory animationFactory) { super(host); mAnimationAdapterFactory = animationFactory; } - private DimState obtainDimState(WindowContainer container) { - if (mDimState == null) { - try { - final SurfaceControl ctl = makeDimLayer(); - mDimState = new DimState(ctl); - } catch (Surface.OutOfResourcesException e) { - Log.w(TAG, "OutOfResourcesException creating dim surface"); - } - } - - mLastRequestedDimContainer = container; - return mDimState; - } - - private SurfaceControl makeDimLayer() { - return mHost.makeChildSurface(null) - .setParent(mHost.getSurfaceControl()) - .setColorLayer() - .setName("Dim Layer for - " + mHost.getName()) - .setCallsite("Dimmer.makeDimLayer") - .build(); - } - - @Override - SurfaceControl getDimLayer() { - return mDimState != null ? mDimState.mDimLayer : null; - } - @Override void resetDimStates() { - if (mDimState == null) { - return; - } - if (!mDimState.mDontReset) { - mDimState.mDimming = false; - } - } - - @Override - Rect getDimBounds() { - return mDimState != null ? mDimState.mDimBounds : null; - } - - @Override - void dontAnimateExit() { if (mDimState != null) { - mDimState.mAnimateExit = false; + mDimState.mLastRequestedDimContainer = null; } } @Override protected void adjustAppearance(WindowContainer container, float alpha, int blurRadius) { final DimState d = obtainDimState(container); - mDimState.setRequestedAppearance(alpha, blurRadius); - d.mDimming = true; + d.prepareLookChange(alpha, blurRadius); } @Override protected void adjustRelativeLayer(WindowContainer container, int relativeLayer) { if (mDimState != null) { - mDimState.setRequestedRelativeParent(container, relativeLayer); + mDimState.prepareReparent(container, relativeLayer); } } + @Override boolean updateDims(SurfaceControl.Transaction t) { if (mDimState == null) { return false; } - - if (!mDimState.mDimming) { - // No one is dimming anymore, fade out dim and remove - if (!mDimState.mAnimateExit) { - if (mDimState.mDimLayer.isValid()) { - t.remove(mDimState.mDimLayer); - } - } else { - mDimState.setExitParameters( - mDimState.mRequestedProperties.mDimmingContainer); - mDimState.applyChanges(t); - } + if (!mDimState.isDimming()) { + // No one is dimming, fade out and remove the dim + mDimState.exit(t); mDimState = null; return false; + } else { + // Someone is dimming, show the requested changes + mDimState.adjustSurfaceLayout(t); + final WindowState ws = mDimState.mLastRequestedDimContainer.asWindowState(); + if (!mDimState.mIsVisible && ws != null && ws.mActivityRecord != null + && ws.mActivityRecord.mStartingData != null) { + // Skip enter animation while starting window is on top of its activity + mDimState.mSkipAnimation = true; + } + mDimState.setReady(t); + return true; } - final Rect bounds = mDimState.mDimBounds; - // TODO: Once we use geometry from hierarchy this falls away. - t.setPosition(mDimState.mDimLayer, bounds.left, bounds.top); - t.setWindowCrop(mDimState.mDimLayer, bounds.width(), bounds.height()); - // Skip enter animation while starting window is on top of its activity - final WindowState ws = mLastRequestedDimContainer.asWindowState(); - if (!mDimState.mIsVisible && ws != null && ws.mActivityRecord != null - && ws.mActivityRecord.mStartingData != null) { - mDimState.mRequestedProperties.mSkipAnimation = true; - } - mDimState.applyChanges(t); - return true; - } - - private long getDimDuration(WindowContainer container) { - // Use the same duration as the animation on the WindowContainer - AnimationAdapter animationAdapter = container.mSurfaceAnimator.getAnimation(); - final float durationScale = container.mWmService.getTransitionAnimationScaleLocked(); - return animationAdapter == null ? (long) (DEFAULT_DIM_ANIM_DURATION * durationScale) - : animationAdapter.getDurationHint(); } - private static class AnimationExtremes { - final T mStartValue; - final T mFinishValue; - - AnimationExtremes(T fromValue, T toValue) { - mStartValue = fromValue; - mFinishValue = toValue; + private DimState obtainDimState(WindowContainer container) { + if (mDimState == null) { + mDimState = new DimState(); } + mDimState.mLastRequestedDimContainer = container; + return mDimState; } - private static class AnimationSpec implements LocalAnimationAdapter.AnimationSpec { - private final long mDuration; - private final AnimationExtremes mAlpha; - private final AnimationExtremes mBlur; - - float mCurrentAlpha = 0; - int mCurrentBlur = 0; - - AnimationSpec(AnimationExtremes alpha, - AnimationExtremes blur, long duration) { - mAlpha = alpha; - mBlur = blur; - mDuration = duration; - } - - @Override - public long getDuration() { - return mDuration; - } - - @Override - public void apply(SurfaceControl.Transaction t, SurfaceControl sc, long currentPlayTime) { - final float fraction = getFraction(currentPlayTime); - mCurrentAlpha = - fraction * (mAlpha.mFinishValue - mAlpha.mStartValue) + mAlpha.mStartValue; - mCurrentBlur = - (int) fraction * (mBlur.mFinishValue - mBlur.mStartValue) + mBlur.mStartValue; - t.setAlpha(sc, mCurrentAlpha); - t.setBackgroundBlurRadius(sc, mCurrentBlur); - } - - @Override - public void dump(PrintWriter pw, String prefix) { - pw.print(prefix); pw.print("from_alpha="); pw.print(mAlpha.mStartValue); - pw.print(" to_alpha="); pw.print(mAlpha.mFinishValue); - pw.print(prefix); pw.print("from_blur="); pw.print(mBlur.mStartValue); - pw.print(" to_blur="); pw.print(mBlur.mFinishValue); - pw.print(" duration="); pw.println(mDuration); - } - - @Override - public void dumpDebugInner(ProtoOutputStream proto) { - final long token = proto.start(ALPHA); - proto.write(FROM, mAlpha.mStartValue); - proto.write(TO, mAlpha.mFinishValue); - proto.write(DURATION_MS, mDuration); - proto.end(token); - } + @Override + @VisibleForTesting + SurfaceControl getDimLayer() { + return mDimState != null ? mDimState.mDimSurface : null; } - static class AnimationAdapterFactory { + @Override + Rect getDimBounds() { + return mDimState != null ? mDimState.mDimBounds : null; + } - public AnimationAdapter get(LocalAnimationAdapter.AnimationSpec alphaAnimationSpec, - SurfaceAnimationRunner runner) { - return new LocalAnimationAdapter(alphaAnimationSpec, runner); + @Override + void dontAnimateExit() { + if (mDimState != null) { + mDimState.mAnimateExit = false; } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java index 9f584911aed7..a831f6a382ed 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java @@ -122,7 +122,8 @@ public class DimmerTests extends WindowTestsBase { } } - static class MockAnimationAdapterFactory extends SmoothDimmer.AnimationAdapterFactory { + static class MockAnimationAdapterFactory extends DimmerAnimationHelper.AnimationAdapterFactory { + @Override public AnimationAdapter get(LocalAnimationAdapter.AnimationSpec alphaAnimationSpec, SurfaceAnimationRunner runner) { return sTestAnimation; -- cgit v1.2.3-59-g8ed1b From 5d858ee59bc35368e92fc62256f5e3219804a441 Mon Sep 17 00:00:00 2001 From: Josep del Rio Date: Tue, 28 Nov 2023 12:31:08 +0000 Subject: Replace Action+Grave with Action+Escape At the moment Action+Grave will go back, but not Action+Escape; Escape is the top-left key, not grave. Bug: 313612728 Test: Flashed build, confirmed Action+Escape goes back Change-Id: Ieb83fb0373f62e39e2debed72cdbc1b6761760ff Flag: NONE --- data/keyboards/Generic.kcm | 2 +- .../android/systemui/statusbar/KeyboardShortcutListSearch.java | 4 ++-- .../java/com/android/server/policy/PhoneWindowManager.java | 2 +- .../src/com/android/server/policy/ShortcutLoggingTests.java | 10 +++++----- 4 files changed, 9 insertions(+), 9 deletions(-) (limited to 'data') diff --git a/data/keyboards/Generic.kcm b/data/keyboards/Generic.kcm index 1048742adb70..e7e174064849 100644 --- a/data/keyboards/Generic.kcm +++ b/data/keyboards/Generic.kcm @@ -500,7 +500,7 @@ key PLUS { key ESCAPE { base: none - alt, meta: fallback HOME + alt: fallback HOME ctrl: fallback MENU } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java index 1b096b592a4a..e5b53d5091fc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java @@ -493,11 +493,11 @@ public final class KeyboardShortcutListSearch { Arrays.asList( Pair.create(KeyEvent.KEYCODE_SLASH, KeyEvent.META_META_ON))), /* Back: go back to previous state (back button) */ - /* Meta + Grave, Meta + backspace, Meta + left arrow */ + /* Meta + Escape, Meta + Grave, Meta + backspace, Meta + left arrow */ new ShortcutKeyGroupMultiMappingInfo( context.getString(R.string.group_system_go_back), Arrays.asList( - Pair.create(KeyEvent.KEYCODE_GRAVE, KeyEvent.META_META_ON), + Pair.create(KeyEvent.KEYCODE_ESCAPE, KeyEvent.META_META_ON), Pair.create(KeyEvent.KEYCODE_DEL, KeyEvent.META_META_ON), Pair.create(KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.META_META_ON))), /* Access home screen: Meta + H, Meta + Enter */ diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 73c422490330..405fe7bec1e8 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -3433,7 +3433,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } break; case KeyEvent.KEYCODE_DEL: - case KeyEvent.KEYCODE_GRAVE: + case KeyEvent.KEYCODE_ESCAPE: if (firstDown && event.isMetaPressed()) { logKeyboardSystemsEvent(event, KeyboardLogEvent.BACK); injectBackGesture(event.getDownTime()); diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java index 71098aa5e883..b5e651c87f1b 100644 --- a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java @@ -30,13 +30,13 @@ import androidx.test.filters.MediumTest; import com.android.internal.annotations.Keep; import com.android.server.input.KeyboardMetricsCollector.KeyboardLogEvent; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import junitparams.JUnitParamsRunner; -import junitparams.Parameters; - @Presubmit @MediumTest @RunWith(JUnitParamsRunner.class) @@ -71,8 +71,8 @@ public class ShortcutLoggingTests extends ShortcutKeyTestBase { KeyboardLogEvent.RECENT_APPS, KeyEvent.KEYCODE_TAB, ALT_ON}, {"BACK key -> Go back", new int[]{KeyEvent.KEYCODE_BACK}, KeyboardLogEvent.BACK, KeyEvent.KEYCODE_BACK, 0}, - {"Meta + `(grave) -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_GRAVE}, - KeyboardLogEvent.BACK, KeyEvent.KEYCODE_GRAVE, META_ON}, + {"Meta + Escape -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_ESCAPE}, + KeyboardLogEvent.BACK, KeyEvent.KEYCODE_ESCAPE, META_ON}, {"Meta + Left arrow -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_DPAD_LEFT}, KeyboardLogEvent.BACK, KeyEvent.KEYCODE_DPAD_LEFT, META_ON}, {"Meta + Del -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_DEL}, -- cgit v1.2.3-59-g8ed1b From 6233caf0c0a6a6b84de34e9607f84d4d1acae5aa Mon Sep 17 00:00:00 2001 From: Nick Chameyev Date: Fri, 29 Sep 2023 16:08:02 +0000 Subject: Defer display switch update if transition is running WindowManager receives updates from DisplayManager through onDisplayChanged() callback by reading the DisplayInfo objects after receiving this callback. This CL makes WindowManager to defer the updates if there is a collecting Shell transition. This is needed to allow starting physical display change transition if there is another transition running. Previously this was silently failing without starting a display switch transition. It also changes the behavior of PhysicalDisplayTransitionLauncher: now it starts the display change transition even if it's not an 'unfold' transition. UnfoldTransitionHandler will decide if it wants to handle 'unfold' display change and default transition handler will be used otherwise. Bug: 259220649 Bug: 277866717 Test: atest PhysicalDisplaySwitchTransitionLauncherTest Test: atest DisplayContentTests Test: manual fold/unfold with apps/split screen/ split screen + PIP Change-Id: Ib0d0624bf141ff16578d7902bec98272d17ee36f --- data/etc/services.core.protolog.json | 24 ++ .../wm/shell/transition/DefaultMixedHandler.java | 3 +- .../wm/shell/unfold/UnfoldTransitionHandler.java | 32 +- .../shell/unfold/UnfoldTransitionHandlerTest.java | 29 +- .../android/server/wm/DeferredDisplayUpdater.java | 345 +++++++++++++++++++++ .../java/com/android/server/wm/DisplayContent.java | 7 +- .../server/wm/utils/DisplayInfoOverrides.java | 6 +- .../server/wm/DeferredDisplayUpdaterDiffTest.java | 267 ++++++++++++++++ .../wm/DisplayContentDeferredUpdateTests.java | 237 ++++++++++++++ 9 files changed, 936 insertions(+), 14 deletions(-) create mode 100644 services/core/java/com/android/server/wm/DeferredDisplayUpdater.java create mode 100644 services/tests/wmtests/src/com/android/server/wm/DeferredDisplayUpdaterDiffTest.java create mode 100644 services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java (limited to 'data') diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 2237ba1924db..1fbbb6e428b6 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -169,6 +169,12 @@ "group": "WM_DEBUG_WINDOW_ORGANIZER", "at": "com\/android\/server\/wm\/TaskOrganizerController.java" }, + "-1961637874": { + "message": "DeferredDisplayUpdater: applying DisplayInfo immediately", + "level": "DEBUG", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/DeferredDisplayUpdater.java" + }, "-1949279037": { "message": "Attempted to add input method window with bad token %s. Aborting.", "level": "WARN", @@ -313,6 +319,12 @@ "group": "WM_DEBUG_RESIZE", "at": "com\/android\/server\/wm\/WindowState.java" }, + "-1818910559": { + "message": "DeferredDisplayUpdater: applied DisplayInfo after deferring", + "level": "DEBUG", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/DeferredDisplayUpdater.java" + }, "-1814361639": { "message": "Set IME snapshot position: (%d, %d)", "level": "INFO", @@ -1909,6 +1921,12 @@ "group": "WM_DEBUG_FOCUS_LIGHT", "at": "com\/android\/server\/wm\/DisplayContent.java" }, + "-415346336": { + "message": "DeferredDisplayUpdater: partially applying DisplayInfo immediately", + "level": "DEBUG", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/DeferredDisplayUpdater.java" + }, "-401282500": { "message": "destroyIfPossible: r=%s destroy returned removed=%s", "level": "DEBUG", @@ -1957,6 +1975,12 @@ "group": "WM_DEBUG_APP_TRANSITIONS", "at": "com\/android\/server\/wm\/AppTransitionController.java" }, + "-376950429": { + "message": "DeferredDisplayUpdater: deferring DisplayInfo update", + "level": "DEBUG", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/DeferredDisplayUpdater.java" + }, "-374767836": { "message": "setAppVisibility(%s, visible=%b): %s visible=%b mVisibleRequested=%b Callers=%s", "level": "VERBOSE", diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index ce7fef2d1fdf..eb301192c90b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -50,7 +50,6 @@ import android.window.WindowContainerTransaction; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.activityembedding.ActivityEmbeddingController; import com.android.wm.shell.common.split.SplitScreenUtils; -import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.pip.PipTransitionController; @@ -298,7 +297,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, mixed.mLeftoversHandler = handler.first; mActiveTransitions.add(mixed); return handler.second; - } else if (mUnfoldHandler != null && mUnfoldHandler.hasUnfold(request)) { + } else if (mUnfoldHandler != null && mUnfoldHandler.shouldPlayUnfoldAnimation(request)) { final WindowContainerTransaction wct = mUnfoldHandler.handleRequest(transition, request); if (wct != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java index 07c54293111c..20ff79f7318e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java @@ -106,7 +106,7 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull TransitionFinishCallback finishCallback) { - if (hasUnfold(info) && transition != mTransition) { + if (shouldPlayUnfoldAnimation(info) && transition != mTransition) { // Take over transition that has unfold, we might receive it if no other handler // accepted request in handleRequest, e.g. for rotation + unfold or // TRANSIT_NONE + unfold transitions @@ -213,14 +213,36 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene } /** Whether `request` contains an unfold action. */ - public boolean hasUnfold(@NonNull TransitionRequestInfo request) { + public boolean shouldPlayUnfoldAnimation(@NonNull TransitionRequestInfo request) { + // Unfold animation won't play when animations are disabled + if (!ValueAnimator.areAnimatorsEnabled()) return false; + return (request.getType() == TRANSIT_CHANGE && request.getDisplayChange() != null - && request.getDisplayChange().isPhysicalDisplayChanged()); + && isUnfoldDisplayChange(request.getDisplayChange())); + } + + private boolean isUnfoldDisplayChange( + @NonNull TransitionRequestInfo.DisplayChange displayChange) { + if (!displayChange.isPhysicalDisplayChanged()) { + return false; + } + + if (displayChange.getStartAbsBounds() == null || displayChange.getEndAbsBounds() == null) { + return false; + } + + // Handle only unfolding, currently we don't have an animation when folding + final int endArea = + displayChange.getEndAbsBounds().width() * displayChange.getEndAbsBounds().height(); + final int startArea = displayChange.getStartAbsBounds().width() + * displayChange.getStartAbsBounds().height(); + + return endArea > startArea; } /** Whether `transitionInfo` contains an unfold action. */ - public boolean hasUnfold(@NonNull TransitionInfo transitionInfo) { + public boolean shouldPlayUnfoldAnimation(@NonNull TransitionInfo transitionInfo) { // Unfold animation won't play when animations are disabled if (!ValueAnimator.areAnimatorsEnabled()) return false; @@ -250,7 +272,7 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request) { - if (hasUnfold(request)) { + if (shouldPlayUnfoldAnimation(request)) { mTransition = transition; return new WindowContainerTransaction(); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java index 6d73c12dc304..fc1fe1cd0acc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java @@ -98,10 +98,13 @@ public class UnfoldTransitionHandlerTest { } @Test - public void handleRequest_physicalDisplayChange_handlesTransition() { + public void handleRequest_physicalDisplayChangeUnfold_handlesTransition() { ActivityManager.RunningTaskInfo triggerTaskInfo = new ActivityManager.RunningTaskInfo(); TransitionRequestInfo.DisplayChange displayChange = new TransitionRequestInfo.DisplayChange( - Display.DEFAULT_DISPLAY).setPhysicalDisplayChanged(true); + Display.DEFAULT_DISPLAY) + .setPhysicalDisplayChanged(true) + .setStartAbsBounds(new Rect(0, 0, 100, 100)) + .setEndAbsBounds(new Rect(0, 0, 200, 200)); TransitionRequestInfo requestInfo = new TransitionRequestInfo(TRANSIT_CHANGE, triggerTaskInfo, /* remoteTransition= */ null, displayChange, 0 /* flags */); @@ -111,6 +114,23 @@ public class UnfoldTransitionHandlerTest { assertThat(result).isNotNull(); } + @Test + public void handleRequest_physicalDisplayChangeFold_doesNotHandleTransition() { + ActivityManager.RunningTaskInfo triggerTaskInfo = new ActivityManager.RunningTaskInfo(); + TransitionRequestInfo.DisplayChange displayChange = new TransitionRequestInfo.DisplayChange( + Display.DEFAULT_DISPLAY) + .setPhysicalDisplayChanged(true) + .setStartAbsBounds(new Rect(0, 0, 200, 200)) + .setEndAbsBounds(new Rect(0, 0, 100, 100)); + TransitionRequestInfo requestInfo = new TransitionRequestInfo(TRANSIT_CHANGE, + triggerTaskInfo, /* remoteTransition= */ null, displayChange, 0 /* flags */); + + WindowContainerTransaction result = mUnfoldTransitionHandler.handleRequest(mTransition, + requestInfo); + + assertThat(result).isNull(); + } + @Test public void handleRequest_noPhysicalDisplayChange_doesNotHandleTransition() { ActivityManager.RunningTaskInfo triggerTaskInfo = new ActivityManager.RunningTaskInfo(); @@ -306,7 +326,10 @@ public class UnfoldTransitionHandlerTest { private TransitionRequestInfo createUnfoldTransitionRequestInfo() { ActivityManager.RunningTaskInfo triggerTaskInfo = new ActivityManager.RunningTaskInfo(); TransitionRequestInfo.DisplayChange displayChange = new TransitionRequestInfo.DisplayChange( - Display.DEFAULT_DISPLAY).setPhysicalDisplayChanged(true); + Display.DEFAULT_DISPLAY) + .setPhysicalDisplayChanged(true) + .setStartAbsBounds(new Rect(0, 0, 100, 100)) + .setEndAbsBounds(new Rect(0, 0, 200, 200)); return new TransitionRequestInfo(TRANSIT_CHANGE, triggerTaskInfo, /* remoteTransition= */ null, displayChange, 0 /* flags */); } diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java new file mode 100644 index 000000000000..975fdc0ade5d --- /dev/null +++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.view.WindowManager.TRANSIT_CHANGE; + +import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS; +import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY; +import static com.android.server.wm.utils.DisplayInfoOverrides.WM_OVERRIDE_FIELDS; +import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFields; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Rect; +import android.view.DisplayInfo; +import android.window.DisplayAreaInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.display.BrightnessSynchronizer; +import com.android.internal.protolog.common.ProtoLog; +import com.android.server.wm.utils.DisplayInfoOverrides.DisplayInfoFieldsUpdater; + +import java.util.Arrays; +import java.util.Objects; + +/** + * A DisplayUpdater that could defer and queue display updates coming from DisplayManager to + * WindowManager. It allows to defer pending display updates if WindowManager is currently not + * ready to apply them. + * For example, this might happen if there is a Shell transition running and physical display + * changed. We can't immediately apply the display updates because we want to start a separate + * display change transition. In this case, we will queue all display updates until the current + * transition's collection finishes and then apply them afterwards. + */ +public class DeferredDisplayUpdater implements DisplayUpdater { + + /** + * List of fields that could be deferred before applying to DisplayContent. + * This should be kept in sync with {@link DeferredDisplayUpdater#calculateDisplayInfoDiff} + */ + @VisibleForTesting + static final DisplayInfoFieldsUpdater DEFERRABLE_FIELDS = (out, override) -> { + // Treat unique id and address change as WM-specific display change as we re-query display + // settings and parameters based on it which could cause window changes + out.uniqueId = override.uniqueId; + out.address = override.address; + + // Also apply WM-override fields, since they might produce differences in window hierarchy + WM_OVERRIDE_FIELDS.setFields(out, override); + }; + + private final DisplayContent mDisplayContent; + + @NonNull + private final DisplayInfo mNonOverrideDisplayInfo = new DisplayInfo(); + + /** + * The last known display parameters from DisplayManager, some WM-specific fields in this object + * might not be applied to the DisplayContent yet + */ + @Nullable + private DisplayInfo mLastDisplayInfo; + + /** + * The last DisplayInfo that was applied to DisplayContent, only WM-specific parameters must be + * used from this object. This object is used to store old values of DisplayInfo while these + * fields are pending to be applied to DisplayContent. + */ + @Nullable + private DisplayInfo mLastWmDisplayInfo; + + @NonNull + private final DisplayInfo mOutputDisplayInfo = new DisplayInfo(); + + public DeferredDisplayUpdater(@NonNull DisplayContent displayContent) { + mDisplayContent = displayContent; + mNonOverrideDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo()); + } + + /** + * Reads the latest display parameters from the display manager and returns them in a callback. + * If there are pending display updates, it will wait for them to finish first and only then it + * will call the callback with the latest display parameters. + * + * @param finishCallback is called when all pending display updates are finished + */ + @Override + public void updateDisplayInfo(@NonNull Runnable finishCallback) { + // Get the latest display parameters from the DisplayManager + final DisplayInfo displayInfo = getCurrentDisplayInfo(); + + final int displayInfoDiff = calculateDisplayInfoDiff(mLastDisplayInfo, displayInfo); + final boolean physicalDisplayUpdated = isPhysicalDisplayUpdated(mLastDisplayInfo, + displayInfo); + + mLastDisplayInfo = displayInfo; + + // Apply whole display info immediately as is if either: + // * it is the first display update + // * shell transitions are disabled or temporary unavailable + if (displayInfoDiff == DIFF_EVERYTHING + || !mDisplayContent.mTransitionController.isShellTransitionsEnabled()) { + ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS, + "DeferredDisplayUpdater: applying DisplayInfo immediately"); + + mLastWmDisplayInfo = displayInfo; + applyLatestDisplayInfo(); + finishCallback.run(); + return; + } + + // If there are non WM-specific display info changes, apply only these fields immediately + if ((displayInfoDiff & DIFF_NOT_WM_DEFERRABLE) > 0) { + ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS, + "DeferredDisplayUpdater: partially applying DisplayInfo immediately"); + applyLatestDisplayInfo(); + } + + // If there are WM-specific display info changes, apply them through a Shell transition + if ((displayInfoDiff & DIFF_WM_DEFERRABLE) > 0) { + ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS, + "DeferredDisplayUpdater: deferring DisplayInfo update"); + + requestDisplayChangeTransition(physicalDisplayUpdated, () -> { + // Apply deferrable fields to DisplayContent only when the transition + // starts collecting, non-deferrable fields are ignored in mLastWmDisplayInfo + mLastWmDisplayInfo = displayInfo; + applyLatestDisplayInfo(); + finishCallback.run(); + }); + } else { + // There are no WM-specific updates, so we can immediately notify that all display + // info changes are applied + finishCallback.run(); + } + } + + /** + * Requests a display change Shell transition + * + * @param physicalDisplayUpdated if true also starts remote display change + * @param onStartCollect called when the Shell transition starts collecting + */ + private void requestDisplayChangeTransition(boolean physicalDisplayUpdated, + @NonNull Runnable onStartCollect) { + + final Transition transition = new Transition(TRANSIT_CHANGE, /* flags= */ 0, + mDisplayContent.mTransitionController, + mDisplayContent.mTransitionController.mSyncEngine); + + mDisplayContent.mAtmService.startPowerMode(POWER_MODE_REASON_CHANGE_DISPLAY); + + mDisplayContent.mTransitionController.startCollectOrQueue(transition, deferred -> { + final Rect startBounds = new Rect(0, 0, mDisplayContent.mInitialDisplayWidth, + mDisplayContent.mInitialDisplayHeight); + final int fromRotation = mDisplayContent.getRotation(); + + onStartCollect.run(); + + ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS, + "DeferredDisplayUpdater: applied DisplayInfo after deferring"); + + if (physicalDisplayUpdated) { + onDisplayUpdated(transition, fromRotation, startBounds); + } else { + transition.setAllReady(); + } + }); + } + + /** + * Applies current DisplayInfo to DisplayContent, DisplayContent is merged from two parts: + * - non-deferrable fields are set from the most recent values received from DisplayManager + * (uses {@link mLastDisplayInfo} field) + * - deferrable fields are set from the latest values that we could apply to WM + * (uses {@link mLastWmDisplayInfo} field) + */ + private void applyLatestDisplayInfo() { + copyDisplayInfoFields(mOutputDisplayInfo, /* base= */ mLastDisplayInfo, + /* override= */ mLastWmDisplayInfo, /* fields= */ DEFERRABLE_FIELDS); + mDisplayContent.onDisplayInfoUpdated(mOutputDisplayInfo); + } + + @NonNull + private DisplayInfo getCurrentDisplayInfo() { + mDisplayContent.mWmService.mDisplayManagerInternal.getNonOverrideDisplayInfo( + mDisplayContent.mDisplayId, mNonOverrideDisplayInfo); + return new DisplayInfo(mNonOverrideDisplayInfo); + } + + /** + * Called when physical display is updated, this could happen e.g. on foldable + * devices when the physical underlying display is replaced. This method should be called + * when the new display info is already applied to the WM hierarchy. + * + * @param fromRotation rotation before the display change + * @param startBounds display bounds before the display change + */ + private void onDisplayUpdated(@NonNull Transition transition, int fromRotation, + @NonNull Rect startBounds) { + final Rect endBounds = new Rect(0, 0, mDisplayContent.mInitialDisplayWidth, + mDisplayContent.mInitialDisplayHeight); + final int toRotation = mDisplayContent.getRotation(); + + final TransitionRequestInfo.DisplayChange displayChange = + new TransitionRequestInfo.DisplayChange(mDisplayContent.getDisplayId()); + displayChange.setStartAbsBounds(startBounds); + displayChange.setEndAbsBounds(endBounds); + displayChange.setStartRotation(fromRotation); + displayChange.setEndRotation(toRotation); + displayChange.setPhysicalDisplayChanged(true); + + mDisplayContent.mTransitionController.requestStartTransition(transition, + /* startTask= */ null, /* remoteTransition= */ null, displayChange); + + final DisplayAreaInfo newDisplayAreaInfo = mDisplayContent.getDisplayAreaInfo(); + + final boolean startedRemoteChange = mDisplayContent.mRemoteDisplayChangeController + .performRemoteDisplayChange(fromRotation, toRotation, newDisplayAreaInfo, + transaction -> finishDisplayUpdate(transaction, transition)); + + if (!startedRemoteChange) { + finishDisplayUpdate(/* wct= */ null, transition); + } + } + + private void finishDisplayUpdate(@Nullable WindowContainerTransaction wct, + @NonNull Transition transition) { + if (wct != null) { + mDisplayContent.mAtmService.mWindowOrganizerController.applyTransaction( + wct); + } + transition.setAllReady(); + } + + private boolean isPhysicalDisplayUpdated(@Nullable DisplayInfo first, + @Nullable DisplayInfo second) { + if (first == null || second == null) return true; + return !Objects.equals(first.uniqueId, second.uniqueId); + } + + /** + * Diff result: fields are the same + */ + static final int DIFF_NONE = 0; + + /** + * Diff result: fields that could be deferred in WM are different + */ + static final int DIFF_WM_DEFERRABLE = 1 << 0; + + /** + * Diff result: fields that could not be deferred in WM are different + */ + static final int DIFF_NOT_WM_DEFERRABLE = 1 << 1; + + /** + * Diff result: everything is different + */ + static final int DIFF_EVERYTHING = 0XFFFFFFFF; + + @VisibleForTesting + static int calculateDisplayInfoDiff(@Nullable DisplayInfo first, @Nullable DisplayInfo second) { + int diff = DIFF_NONE; + + if (Objects.equals(first, second)) return diff; + if (first == null || second == null) return DIFF_EVERYTHING; + + if (first.layerStack != second.layerStack + || first.flags != second.flags + || first.type != second.type + || first.displayId != second.displayId + || first.displayGroupId != second.displayGroupId + || !Objects.equals(first.deviceProductInfo, second.deviceProductInfo) + || first.modeId != second.modeId + || first.renderFrameRate != second.renderFrameRate + || first.defaultModeId != second.defaultModeId + || first.userPreferredModeId != second.userPreferredModeId + || !Arrays.equals(first.supportedModes, second.supportedModes) + || first.colorMode != second.colorMode + || !Arrays.equals(first.supportedColorModes, second.supportedColorModes) + || !Objects.equals(first.hdrCapabilities, second.hdrCapabilities) + || !Arrays.equals(first.userDisabledHdrTypes, second.userDisabledHdrTypes) + || first.minimalPostProcessingSupported != second.minimalPostProcessingSupported + || first.appVsyncOffsetNanos != second.appVsyncOffsetNanos + || first.presentationDeadlineNanos != second.presentationDeadlineNanos + || first.state != second.state + || first.committedState != second.committedState + || first.ownerUid != second.ownerUid + || !Objects.equals(first.ownerPackageName, second.ownerPackageName) + || first.removeMode != second.removeMode + || first.getRefreshRate() != second.getRefreshRate() + || first.brightnessMinimum != second.brightnessMinimum + || first.brightnessMaximum != second.brightnessMaximum + || first.brightnessDefault != second.brightnessDefault + || first.installOrientation != second.installOrientation + || !Objects.equals(first.layoutLimitedRefreshRate, second.layoutLimitedRefreshRate) + || !BrightnessSynchronizer.floatEquals(first.hdrSdrRatio, second.hdrSdrRatio) + || !first.thermalRefreshRateThrottling.contentEquals( + second.thermalRefreshRateThrottling) + || !Objects.equals(first.thermalBrightnessThrottlingDataId, + second.thermalBrightnessThrottlingDataId)) { + diff |= DIFF_NOT_WM_DEFERRABLE; + } + + if (first.appWidth != second.appWidth + || first.appHeight != second.appHeight + || first.smallestNominalAppWidth != second.smallestNominalAppWidth + || first.smallestNominalAppHeight != second.smallestNominalAppHeight + || first.largestNominalAppWidth != second.largestNominalAppWidth + || first.largestNominalAppHeight != second.largestNominalAppHeight + || first.logicalWidth != second.logicalWidth + || first.logicalHeight != second.logicalHeight + || first.physicalXDpi != second.physicalXDpi + || first.physicalYDpi != second.physicalYDpi + || first.rotation != second.rotation + || !Objects.equals(first.displayCutout, second.displayCutout) + || first.logicalDensityDpi != second.logicalDensityDpi + || !Objects.equals(first.roundedCorners, second.roundedCorners) + || !Objects.equals(first.displayShape, second.displayShape) + || !Objects.equals(first.uniqueId, second.uniqueId) + || !Objects.equals(first.address, second.address) + ) { + diff |= DIFF_WM_DEFERRABLE; + } + + return diff; + } +} diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 50376fed2005..200e0aead4b6 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -276,6 +276,7 @@ import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; +import static com.android.window.flags.Flags.deferDisplayUpdates; /** * Utility class for keeping track of the WindowStates and other pertinent contents of a @@ -1158,7 +1159,11 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mWallpaperController.resetLargestDisplay(display); display.getDisplayInfo(mDisplayInfo); display.getMetrics(mDisplayMetrics); - mDisplayUpdater = new ImmediateDisplayUpdater(this); + if (deferDisplayUpdates()) { + mDisplayUpdater = new DeferredDisplayUpdater(this); + } else { + mDisplayUpdater = new ImmediateDisplayUpdater(this); + } mSystemGestureExclusionLimit = mWmService.mConstants.mSystemGestureExclusionLimitDp * mDisplayMetrics.densityDpi / DENSITY_DEFAULT; isDefaultDisplay = mDisplayId == DEFAULT_DISPLAY; diff --git a/services/core/java/com/android/server/wm/utils/DisplayInfoOverrides.java b/services/core/java/com/android/server/wm/utils/DisplayInfoOverrides.java index 8c8f6a6cb386..193a0c8be60b 100644 --- a/services/core/java/com/android/server/wm/utils/DisplayInfoOverrides.java +++ b/services/core/java/com/android/server/wm/utils/DisplayInfoOverrides.java @@ -30,7 +30,7 @@ public class DisplayInfoOverrides { * Set of DisplayInfo fields that are overridden in DisplayManager using values from * WindowManager */ - public static final DisplayInfoFields WM_OVERRIDE_FIELDS = (out, source) -> { + public static final DisplayInfoFieldsUpdater WM_OVERRIDE_FIELDS = (out, source) -> { out.appWidth = source.appWidth; out.appHeight = source.appHeight; out.smallestNominalAppWidth = source.smallestNominalAppWidth; @@ -55,7 +55,7 @@ public class DisplayInfoOverrides { public static void copyDisplayInfoFields(@NonNull DisplayInfo out, @NonNull DisplayInfo base, @Nullable DisplayInfo override, - @NonNull DisplayInfoFields fields) { + @NonNull DisplayInfoFieldsUpdater fields) { out.copyFrom(base); if (override != null) { @@ -66,7 +66,7 @@ public class DisplayInfoOverrides { /** * Callback interface that allows to specify a subset of fields of DisplayInfo object */ - public interface DisplayInfoFields { + public interface DisplayInfoFieldsUpdater { /** * Copies a subset of fields from {@param source} to {@param out} * diff --git a/services/tests/wmtests/src/com/android/server/wm/DeferredDisplayUpdaterDiffTest.java b/services/tests/wmtests/src/com/android/server/wm/DeferredDisplayUpdaterDiffTest.java new file mode 100644 index 000000000000..44b69f18eb04 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/DeferredDisplayUpdaterDiffTest.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.hardware.display.DeviceProductInfo.CONNECTION_TO_SINK_UNKNOWN; +import static android.view.RoundedCorner.POSITION_TOP_LEFT; +import static android.view.RoundedCorners.NO_ROUNDED_CORNERS; + +import static com.android.server.wm.DeferredDisplayUpdater.DEFERRABLE_FIELDS; +import static com.android.server.wm.DeferredDisplayUpdater.DIFF_NOT_WM_DEFERRABLE; +import static com.android.server.wm.DeferredDisplayUpdater.DIFF_WM_DEFERRABLE; +import static com.android.server.wm.DeferredDisplayUpdater.calculateDisplayInfoDiff; +import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFields; + +import static com.google.common.truth.Truth.assertWithMessage; + +import android.annotation.NonNull; +import android.graphics.Insets; +import android.graphics.Rect; +import android.hardware.display.DeviceProductInfo; +import android.platform.test.annotations.Presubmit; +import android.util.SparseArray; +import android.view.Display; +import android.view.DisplayAddress; +import android.view.DisplayCutout; +import android.view.DisplayInfo; +import android.view.DisplayShape; +import android.view.RoundedCorner; +import android.view.RoundedCorners; +import android.view.SurfaceControl.RefreshRateRange; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; + +/** + * Build/Install/Run: + * atest WmTests:DeferredDisplayUpdaterDiffTest + */ +@SmallTest +@Presubmit +public class DeferredDisplayUpdaterDiffTest { + + private static final Set IGNORED_FIELDS = new HashSet<>(Arrays.asList( + "name" // human-readable name is ignored in equals() checks + )); + + private static final DisplayInfo EMPTY = new DisplayInfo(); + + @Test + public void testCalculateDisplayInfoDiff_allDifferent_returnsChanges() { + final DisplayInfo first = new DisplayInfo(); + final DisplayInfo second = new DisplayInfo(); + makeAllFieldsDifferent(first, second); + + int diff = calculateDisplayInfoDiff(first, second); + + assertWithMessage("Expected to receive a non-zero difference when " + + "there are changes in all fields of DisplayInfo\n" + + "Make sure that you have updated calculateDisplayInfoDiff function after " + + "changing DisplayInfo fields").that(diff).isGreaterThan(0); + } + + @Test + public void testCalculateDisplayInfoDiff_forEveryDifferentField_returnsChanges() { + generateWithSingleDifferentField((first, second, field) -> { + int diff = calculateDisplayInfoDiff(first, second); + + assertWithMessage("Expected to receive a non-zero difference when " + + "there are changes in " + field + "\n" + + "Make sure that you have updated calculateDisplayInfoDiff function after " + + "changing DisplayInfo fields").that(diff).isGreaterThan(0); + }); + } + + @Test + public void testCalculateDisplayInfoDiff_forEveryDifferentField_returnsMatchingChange() { + generateWithSingleDifferentField((first, second, field) -> { + boolean hasDeferrableFieldChange = hasDeferrableFieldChange(first, second); + int expectedDiff = + hasDeferrableFieldChange ? DIFF_WM_DEFERRABLE : DIFF_NOT_WM_DEFERRABLE; + + int diff = calculateDisplayInfoDiff(first, second); + + assertWithMessage("Expected to have diff = " + expectedDiff + + ", for field = " + field + "\n" + + "Make sure that you have updated calculateDisplayInfoDiff function after " + + "changing DisplayInfo fields").that( + diff).isEqualTo(expectedDiff); + }); + } + + /** + * Sets each field of the objects to different values using reflection + */ + private static void makeAllFieldsDifferent(@NonNull DisplayInfo first, + @NonNull DisplayInfo second) { + forEachDisplayInfoField(field -> { + try { + setDifferentFieldValues(first, second, field); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }); + } + + private static boolean hasDeferrableFieldChange(@NonNull DisplayInfo first, + @NonNull DisplayInfo second) { + final DisplayInfo firstDeferrableFieldsOnly = new DisplayInfo(); + final DisplayInfo secondDeferrableFieldsOnly = new DisplayInfo(); + + copyDisplayInfoFields(/* out= */ firstDeferrableFieldsOnly, /* base= */ + EMPTY, /* override= */ first, DEFERRABLE_FIELDS); + copyDisplayInfoFields(/* out= */ secondDeferrableFieldsOnly, /* base= */ + EMPTY, /* override= */ second, DEFERRABLE_FIELDS); + + return !firstDeferrableFieldsOnly.equals(secondDeferrableFieldsOnly); + } + + /** + * Creates pairs of DisplayInfos where only one field is different, the callback is called for + * each field + */ + private static void generateWithSingleDifferentField(DisplayInfoConsumer consumer) { + forEachDisplayInfoField(field -> { + final DisplayInfo first = new DisplayInfo(); + final DisplayInfo second = new DisplayInfo(); + + try { + setDifferentFieldValues(first, second, field); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + + consumer.consume(first, second, field); + }); + } + + private static void setDifferentFieldValues(@NonNull DisplayInfo first, + @NonNull DisplayInfo second, + @NonNull Field field) throws IllegalAccessException { + final Class type = field.getType(); + if (type.equals(int.class)) { + field.setInt(first, 1); + field.setInt(second, 2); + } else if (type.equals(double.class)) { + field.setDouble(first, 1.0); + field.setDouble(second, 2.0); + } else if (type.equals(short.class)) { + field.setShort(first, (short) 1); + field.setShort(second, (short) 2); + } else if (type.equals(long.class)) { + field.setLong(first, 1L); + field.setLong(second, 2L); + } else if (type.equals(char.class)) { + field.setChar(first, 'a'); + field.setChar(second, 'b'); + } else if (type.equals(byte.class)) { + field.setByte(first, (byte) 1); + field.setByte(second, (byte) 2); + } else if (type.equals(float.class)) { + field.setFloat(first, 1.0f); + field.setFloat(second, 2.0f); + } else if (type == boolean.class) { + field.setBoolean(first, true); + field.setBoolean(second, false); + } else if (type.equals(String.class)) { + field.set(first, "one"); + field.set(second, "two"); + } else if (type.equals(DisplayAddress.class)) { + field.set(first, DisplayAddress.fromPhysicalDisplayId(0)); + field.set(second, DisplayAddress.fromPhysicalDisplayId(1)); + } else if (type.equals(DeviceProductInfo.class)) { + field.set(first, new DeviceProductInfo("name", "pnp_id", "product_id1", 2023, + CONNECTION_TO_SINK_UNKNOWN)); + field.set(second, new DeviceProductInfo("name", "pnp_id", "product_id2", 2023, + CONNECTION_TO_SINK_UNKNOWN)); + } else if (type.equals(DisplayCutout.class)) { + field.set(first, + new DisplayCutout(Insets.NONE, new Rect(0, 0, 100, 100), null, null, + null)); + field.set(second, + new DisplayCutout(Insets.NONE, new Rect(0, 0, 200, 200), null, null, + null)); + } else if (type.equals(RoundedCorners.class)) { + field.set(first, NO_ROUNDED_CORNERS); + + final RoundedCorners other = new RoundedCorners(NO_ROUNDED_CORNERS); + other.setRoundedCorner(POSITION_TOP_LEFT, + new RoundedCorner(POSITION_TOP_LEFT, 1, 2, 3)); + field.set(second, other); + } else if (type.equals(DisplayShape.class)) { + field.set(first, DisplayShape.createDefaultDisplayShape(100, 200, false)); + field.set(second, DisplayShape.createDefaultDisplayShape(50, 100, false)); + } else if (type.equals(RefreshRateRange.class)) { + field.set(first, new RefreshRateRange(0, 100)); + field.set(second, new RefreshRateRange(20, 80)); + } else if (type.equals(Display.HdrCapabilities.class)) { + field.set(first, new Display.HdrCapabilities(new int[]{0}, 100, 50, 25)); + field.set(second, new Display.HdrCapabilities(new int[]{1}, 100, 50, 25)); + } else if (type.equals(SparseArray.class) + && ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0].equals( + RefreshRateRange.class)) { + final SparseArray array1 = new SparseArray<>(); + array1.set(0, new RefreshRateRange(0, 100)); + final SparseArray array2 = new SparseArray<>(); + array2.set(0, new RefreshRateRange(20, 80)); + field.set(first, array1); + field.set(second, array2); + } else if (type.isArray() && type.getComponentType().equals(int.class)) { + field.set(first, new int[]{0}); + field.set(second, new int[]{1}); + } else if (type.isArray() && type.getComponentType().equals(Display.Mode.class)) { + field.set(first, new Display.Mode[]{new Display.Mode(100, 200, 300)}); + field.set(second, new Display.Mode[]{new Display.Mode(10, 20, 30)}); + } else { + throw new IllegalArgumentException("Field " + field + + " is not supported by this test, please add implementation of setting " + + "different values for this field"); + } + } + + private interface DisplayInfoConsumer { + void consume(DisplayInfo first, DisplayInfo second, Field field); + } + + /** + * Iterates over every non-static field of DisplayInfo class except IGNORED_FIELDS + */ + private static void forEachDisplayInfoField(Consumer consumer) { + for (Field field : DisplayInfo.class.getDeclaredFields()) { + field.setAccessible(true); + + if (Modifier.isStatic(field.getModifiers())) { + continue; + } + + if (IGNORED_FIELDS.contains(field.getName())) { + continue; + } + + consumer.accept(field); + } + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java new file mode 100644 index 000000000000..dfa595c23e44 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +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.spyOn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +import android.platform.test.annotations.Presubmit; +import android.view.DisplayInfo; + +import androidx.test.filters.SmallTest; + +import com.android.server.wm.TransitionController.OnStartCollect; +import com.android.window.flags.Flags; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; + +/** + * Tests for the {@link DisplayContent} class when FLAG_DEFER_DISPLAY_UPDATES is enabled. + * + * Build/Install/Run: + * atest WmTests:DisplayContentDeferredUpdateTests + */ +@SmallTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class DisplayContentDeferredUpdateTests extends WindowTestsBase { + + @Override + protected void onBeforeSystemServicesCreated() { + // Set other flags to their default values + mSetFlagsRule.initAllFlagsToReleaseConfigDefault(); + + mSetFlagsRule.enableFlags(Flags.FLAG_DEFER_DISPLAY_UPDATES); + } + + @Before + public void before() { + mockTransitionsController(/* enabled= */ true); + mockRemoteDisplayChangeController(); + } + + @Test + public void testUpdate_deferrableFieldChangedTransitionStarted_deferrableFieldUpdated() { + performInitialDisplayUpdate(); + + givenDisplayInfo(/* uniqueId= */ "old"); + Runnable onUpdated = mock(Runnable.class); + mDisplayContent.requestDisplayUpdate(onUpdated); + + // Emulate that collection has started + captureStartTransitionCollection().getValue().onCollectStarted(/* deferred= */ true); + verify(onUpdated).run(); + clearInvocations(mDisplayContent.mTransitionController, onUpdated); + + givenDisplayInfo(/* uniqueId= */ "new"); + mDisplayContent.requestDisplayUpdate(onUpdated); + captureStartTransitionCollection().getValue().onCollectStarted(/* deferred= */ true); + verify(onUpdated).run(); + assertThat(mDisplayContent.getDisplayInfo().uniqueId).isEqualTo("new"); + } + + @Test + public void testUpdate_nonDeferrableUpdateAndTransitionDeferred_nonDeferrableFieldUpdated() { + performInitialDisplayUpdate(); + + // Update only color mode (non-deferrable field) and keep the same unique id + givenDisplayInfo(/* uniqueId= */ "initial_unique_id", /* colorMode= */ 123); + Runnable onUpdated = mock(Runnable.class); + mDisplayContent.requestDisplayUpdate(onUpdated); + + verify(onUpdated).run(); + assertThat(mDisplayContent.getDisplayInfo().colorMode).isEqualTo(123); + } + + @Test + public void testUpdate_nonDeferrableUpdateTwiceAndTransitionDeferred_fieldHasLatestValue() { + performInitialDisplayUpdate(); + + // Update only color mode (non-deferrable field) and keep the same unique id + givenDisplayInfo(/* uniqueId= */ "initial_unique_id", /* colorMode= */ 123); + mDisplayContent.requestDisplayUpdate(mock(Runnable.class)); + + assertThat(mDisplayContent.getDisplayInfo().colorMode).isEqualTo(123); + assertThat(mDisplayContent.getDisplayInfo().uniqueId) + .isEqualTo("initial_unique_id"); + + // Update unique id (deferrable field), keep the same color mode, + // this update should be deferred + givenDisplayInfo(/* uniqueId= */ "new_unique_id", /* colorMode= */ 123); + mDisplayContent.requestDisplayUpdate(mock(Runnable.class)); + + assertThat(mDisplayContent.getDisplayInfo().colorMode).isEqualTo(123); + assertThat(mDisplayContent.getDisplayInfo().uniqueId) + .isEqualTo("initial_unique_id"); + + // Update color mode again and keep the same unique id, color mode update + // should not be deferred, unique id update is still deferred as transition + // has not started collecting yet + givenDisplayInfo(/* uniqueId= */ "new_unique_id", /* colorMode= */ 456); + Runnable onUpdated = mock(Runnable.class); + mDisplayContent.requestDisplayUpdate(onUpdated); + + assertThat(mDisplayContent.getDisplayInfo().colorMode).isEqualTo(456); + assertThat(mDisplayContent.getDisplayInfo().uniqueId) + .isEqualTo("initial_unique_id"); + + // Mark transition as started collected, so pending changes are applied + captureStartTransitionCollection().getValue().onCollectStarted(/* deferred= */ true); + + // Verify that all fields have the latest values + verify(onUpdated).run(); + assertThat(mDisplayContent.getDisplayInfo().colorMode).isEqualTo(456); + assertThat(mDisplayContent.getDisplayInfo().uniqueId).isEqualTo("new_unique_id"); + } + + @Test + public void testUpdate_deferrableFieldUpdatedTransitionPending_fieldNotUpdated() { + performInitialDisplayUpdate(); + givenDisplayInfo(/* uniqueId= */ "old"); + Runnable onUpdated = mock(Runnable.class); + mDisplayContent.requestDisplayUpdate(onUpdated); + captureStartTransitionCollection().getValue().onCollectStarted(/* deferred= */ true); + verify(onUpdated).run(); + clearInvocations(mDisplayContent.mTransitionController, onUpdated); + + givenDisplayInfo(/* uniqueId= */ "new"); + mDisplayContent.requestDisplayUpdate(onUpdated); + + captureStartTransitionCollection(); // do not continue by not starting the collection + verify(onUpdated, never()).run(); + assertThat(mDisplayContent.getDisplayInfo().uniqueId).isEqualTo("old"); + } + + @Test + public void testTwoDisplayUpdates_transitionStarted_displayUpdated() { + performInitialDisplayUpdate(); + givenDisplayInfo(/* uniqueId= */ "old"); + Runnable onUpdated = mock(Runnable.class); + mDisplayContent.requestDisplayUpdate(onUpdated); + captureStartTransitionCollection().getValue() + .onCollectStarted(/* deferred= */ true); + verify(onUpdated).run(); + clearInvocations(mDisplayContent.mTransitionController, onUpdated); + + // Perform two display updates while WM is 'busy' + givenDisplayInfo(/* uniqueId= */ "new1"); + Runnable onUpdated1 = mock(Runnable.class); + mDisplayContent.requestDisplayUpdate(onUpdated1); + givenDisplayInfo(/* uniqueId= */ "new2"); + Runnable onUpdated2 = mock(Runnable.class); + mDisplayContent.requestDisplayUpdate(onUpdated2); + + // Continue with the first update + captureStartTransitionCollection().getAllValues().get(0) + .onCollectStarted(/* deferred= */ true); + verify(onUpdated1).run(); + verify(onUpdated2, never()).run(); + assertThat(mDisplayContent.getDisplayInfo().uniqueId).isEqualTo("new1"); + + // Continue with the second update + captureStartTransitionCollection().getAllValues().get(1) + .onCollectStarted(/* deferred= */ true); + verify(onUpdated2).run(); + assertThat(mDisplayContent.getDisplayInfo().uniqueId).isEqualTo("new2"); + } + + private void mockTransitionsController(boolean enabled) { + spyOn(mDisplayContent.mTransitionController); + when(mDisplayContent.mTransitionController.isShellTransitionsEnabled()).thenReturn(enabled); + doReturn(true).when(mDisplayContent.mTransitionController).startCollectOrQueue(any(), + any()); + } + + private void mockRemoteDisplayChangeController() { + spyOn(mDisplayContent.mRemoteDisplayChangeController); + doReturn(true).when(mDisplayContent.mRemoteDisplayChangeController) + .performRemoteDisplayChange(anyInt(), anyInt(), any(), any()); + } + + private ArgumentCaptor captureStartTransitionCollection() { + ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(OnStartCollect.class); + verify(mDisplayContent.mTransitionController, atLeast(1)).startCollectOrQueue(any(), + callbackCaptor.capture()); + return callbackCaptor; + } + + private void givenDisplayInfo(String uniqueId) { + givenDisplayInfo(uniqueId, /* colorMode= */ 0); + } + + private void givenDisplayInfo(String uniqueId, int colorMode) { + spyOn(mDisplayContent.mDisplay); + doAnswer(invocation -> { + DisplayInfo info = invocation.getArgument(0); + info.uniqueId = uniqueId; + info.colorMode = colorMode; + return null; + }).when(mDisplayContent.mDisplay).getDisplayInfo(any()); + } + + private void performInitialDisplayUpdate() { + givenDisplayInfo(/* uniqueId= */ "initial_unique_id", /* colorMode= */ 0); + Runnable onUpdated = mock(Runnable.class); + mDisplayContent.requestDisplayUpdate(onUpdated); + } +} -- cgit v1.2.3-59-g8ed1b From ae3a53650fe557f3c7d89dd231aecbde6de4062a Mon Sep 17 00:00:00 2001 From: Vishnu Nair Date: Wed, 29 Nov 2023 05:51:06 +0000 Subject: Use WindowInfosListener for TPL Move TPL logic from surfaceflinger to system server. When a TPL is registered, start listening to window info updates and start checking for whether thresholds will be crossed. This allows us to move the logic away from the composition hot path and optimize to handle multiple listeners. This change also consolidates all the TPL logic into its own thread. Bug: 290795410 Test: atest TrustedPresentationCallbackTest Change-Id: Ib649f157ddbb6b5a952cedeb235f6ace6f5e43ae --- core/java/android/view/AttachedSurfaceControl.java | 39 -- core/java/android/view/IWindowManager.aidl | 8 + core/java/android/view/ViewRootImpl.java | 12 - core/java/android/view/WindowManager.java | 33 ++ core/java/android/view/WindowManagerGlobal.java | 94 ++++- core/java/android/view/WindowManagerImpl.java | 14 + .../window/ITrustedPresentationListener.aidl | 24 ++ .../window/TrustedPresentationListener.java | 26 ++ .../window/TrustedPresentationThresholds.aidl | 3 + .../window/TrustedPresentationThresholds.java | 127 ++++++ .../android/internal/protolog/ProtoLogGroup.java | 1 + data/etc/services.core.protolog.json | 81 ++++ .../wm/TrustedPresentationListenerController.java | 448 +++++++++++++++++++++ .../android/server/wm/WindowManagerService.java | 19 + .../java/com/android/server/wm/WindowState.java | 8 + services/tests/wmtests/AndroidManifest.xml | 2 +- .../server/wm/TrustedPresentationCallbackTest.java | 154 ------- .../server/wm/TrustedPresentationListenerTest.java | 267 ++++++++++++ 18 files changed, 1151 insertions(+), 209 deletions(-) create mode 100644 core/java/android/window/ITrustedPresentationListener.aidl create mode 100644 core/java/android/window/TrustedPresentationListener.java create mode 100644 core/java/android/window/TrustedPresentationThresholds.aidl create mode 100644 core/java/android/window/TrustedPresentationThresholds.java create mode 100644 services/core/java/com/android/server/wm/TrustedPresentationListenerController.java delete mode 100644 services/tests/wmtests/src/com/android/server/wm/TrustedPresentationCallbackTest.java create mode 100644 services/tests/wmtests/src/com/android/server/wm/TrustedPresentationListenerTest.java (limited to 'data') diff --git a/core/java/android/view/AttachedSurfaceControl.java b/core/java/android/view/AttachedSurfaceControl.java index fd5517d29d74..f28574ecb3b2 100644 --- a/core/java/android/view/AttachedSurfaceControl.java +++ b/core/java/android/view/AttachedSurfaceControl.java @@ -27,9 +27,6 @@ import android.window.SurfaceSyncGroup; import com.android.window.flags.Flags; -import java.util.concurrent.Executor; -import java.util.function.Consumer; - /** * Provides an interface to the root-Surface of a View Hierarchy or Window. This * is used in combination with the {@link android.view.SurfaceControl} API to enable @@ -196,42 +193,6 @@ public interface AttachedSurfaceControl { + "implemented before making this call."); } - /** - * Add a trusted presentation listener on the SurfaceControl associated with this window. - * - * @param t Transaction that the trusted presentation listener is added on. This should - * be applied by the caller. - * @param thresholds The {@link SurfaceControl.TrustedPresentationThresholds} that will specify - * when the to invoke the callback. - * @param executor The {@link Executor} where the callback will be invoked on. - * @param listener The {@link Consumer} that will receive the callbacks when entered or - * exited the threshold. - * - * @see SurfaceControl.Transaction#setTrustedPresentationCallback(SurfaceControl, - * SurfaceControl.TrustedPresentationThresholds, Executor, Consumer) - * - * @hide b/287076178 un-hide with API bump - */ - default void addTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t, - @NonNull SurfaceControl.TrustedPresentationThresholds thresholds, - @NonNull Executor executor, @NonNull Consumer listener) { - } - - /** - * Remove a trusted presentation listener on the SurfaceControl associated with this window. - * - * @param t Transaction that the trusted presentation listener removed on. This should - * be applied by the caller. - * @param listener The {@link Consumer} that was previously registered with - * addTrustedPresentationCallback that should be removed. - * - * @see SurfaceControl.Transaction#clearTrustedPresentationCallback(SurfaceControl) - * @hide b/287076178 un-hide with API bump - */ - default void removeTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t, - @NonNull Consumer listener) { - } - /** * Transfer the currently in progress touch gesture from the host to the requested * {@link SurfaceControlViewHost.SurfacePackage}. This requires that the diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 17bbee6d020f..36b74e39072a 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -73,6 +73,8 @@ import android.window.ISurfaceSyncGroupCompletedListener; import android.window.ITaskFpsCallback; import android.window.ScreenCapture; import android.window.WindowContextInfo; +import android.window.ITrustedPresentationListener; +import android.window.TrustedPresentationThresholds; /** * System private interface to the window manager. @@ -1075,4 +1077,10 @@ interface IWindowManager @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MONITOR_INPUT)") void unregisterDecorViewGestureListener(IDecorViewGestureListener listener, int displayId); + + void registerTrustedPresentationListener(in IBinder window, in ITrustedPresentationListener listener, + in TrustedPresentationThresholds thresholds, int id); + + + void unregisterTrustedPresentationListener(in ITrustedPresentationListener listener, int id); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index cac5387116a1..8acab2a43823 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -11913,18 +11913,6 @@ public final class ViewRootImpl implements ViewParent, scheduleTraversals(); } - @Override - public void addTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t, - @NonNull SurfaceControl.TrustedPresentationThresholds thresholds, - @NonNull Executor executor, @NonNull Consumer listener) { - t.setTrustedPresentationCallback(getSurfaceControl(), thresholds, executor, listener); - } - - @Override - public void removeTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t, - @NonNull Consumer listener) { - t.clearTrustedPresentationCallback(getSurfaceControl()); - } private void logAndTrace(String msg) { if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 046ea77f196d..f668088e6b44 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -122,7 +122,9 @@ import android.view.WindowInsets.Side.InsetsSide; import android.view.WindowInsets.Type; import android.view.WindowInsets.Type.InsetsType; import android.view.accessibility.AccessibilityNodeInfo; +import android.window.ITrustedPresentationListener; import android.window.TaskFpsCallback; +import android.window.TrustedPresentationThresholds; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -5884,4 +5886,35 @@ public interface WindowManager extends ViewManager { default boolean replaceContentOnDisplayWithSc(int displayId, @NonNull SurfaceControl sc) { throw new UnsupportedOperationException(); } + + /** + * Add a trusted presentation listener associated with a window. If the listener has already + * been registered, an AndroidRuntimeException will be thrown. + * + * @param window The Window to add the trusted presentation listener for + * @param thresholds The {@link TrustedPresentationThresholds} that will specify + * when the to invoke the callback. + * @param executor The {@link Executor} where the callback will be invoked on. + * @param listener The {@link ITrustedPresentationListener} that will receive the callbacks + * when entered or exited trusted presentation per the thresholds. + * + * @hide b/287076178 un-hide with API bump + */ + default void registerTrustedPresentationListener(@NonNull IBinder window, + @NonNull TrustedPresentationThresholds thresholds, @NonNull Executor executor, + @NonNull Consumer listener) { + throw new UnsupportedOperationException(); + } + + /** + * Removes a presentation listener associated with a window. If the listener was not previously + * registered, the call will be a noop. + * + * @hide + * @see #registerTrustedPresentationListener(IBinder, + * TrustedPresentationThresholds, Executor, Consumer) + */ + default void unregisterTrustedPresentationListener(@NonNull Consumer listener) { + throw new UnsupportedOperationException(); + } } diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java index 214f1ec3d1ec..a7d814e9ab8c 100644 --- a/core/java/android/view/WindowManagerGlobal.java +++ b/core/java/android/view/WindowManagerGlobal.java @@ -30,9 +30,13 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemProperties; import android.util.AndroidRuntimeException; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; +import android.util.Pair; import android.view.inputmethod.InputMethodManager; +import android.window.ITrustedPresentationListener; +import android.window.TrustedPresentationThresholds; import com.android.internal.util.FastPrintWriter; @@ -43,6 +47,7 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.WeakHashMap; import java.util.concurrent.Executor; +import java.util.function.Consumer; import java.util.function.IntConsumer; /** @@ -143,6 +148,9 @@ public final class WindowManagerGlobal { private Runnable mSystemPropertyUpdater; + private final TrustedPresentationListener mTrustedPresentationListener = + new TrustedPresentationListener(); + private WindowManagerGlobal() { } @@ -324,7 +332,7 @@ public final class WindowManagerGlobal { final Context context = view.getContext(); if (context != null && (context.getApplicationInfo().flags - & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) { + & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) { wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; } } @@ -482,7 +490,7 @@ public final class WindowManagerGlobal { if (who != null) { WindowLeaked leak = new WindowLeaked( what + " " + who + " has leaked window " - + root.getView() + " that was originally added here"); + + root.getView() + " that was originally added here"); leak.setStackTrace(root.getLocation().getStackTrace()); Log.e(TAG, "", leak); } @@ -790,6 +798,86 @@ public final class WindowManagerGlobal { } } + public void registerTrustedPresentationListener(@NonNull IBinder window, + @NonNull TrustedPresentationThresholds thresholds, Executor executor, + @NonNull Consumer listener) { + mTrustedPresentationListener.addListener(window, thresholds, listener, executor); + } + + public void unregisterTrustedPresentationListener(@NonNull Consumer listener) { + mTrustedPresentationListener.removeListener(listener); + } + + private final class TrustedPresentationListener extends + ITrustedPresentationListener.Stub { + private static int sId = 0; + private final ArrayMap, Pair> mListeners = + new ArrayMap<>(); + + private final Object mTplLock = new Object(); + + private void addListener(IBinder window, TrustedPresentationThresholds thresholds, + Consumer listener, Executor executor) { + synchronized (mTplLock) { + if (mListeners.containsKey(listener)) { + throw new AndroidRuntimeException("Trying to add duplicate listener"); + } + int id = sId++; + mListeners.put(listener, new Pair<>(id, executor)); + try { + WindowManagerGlobal.getWindowManagerService() + .registerTrustedPresentationListener(window, this, thresholds, id); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + } + + private void removeListener(Consumer listener) { + synchronized (mTplLock) { + var removedListener = mListeners.remove(listener); + if (removedListener == null) { + Log.i(TAG, "listener " + listener + " does not exist."); + return; + } + + try { + WindowManagerGlobal.getWindowManagerService() + .unregisterTrustedPresentationListener(this, removedListener.first); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + } + + @Override + public void onTrustedPresentationChanged(int[] inTrustedStateListenerIds, + int[] outOfTrustedStateListenerIds) { + ArrayList firedListeners = new ArrayList<>(); + synchronized (mTplLock) { + mListeners.forEach((listener, idExecutorPair) -> { + final var listenerId = idExecutorPair.first; + final var executor = idExecutorPair.second; + for (int id : inTrustedStateListenerIds) { + if (listenerId == id) { + firedListeners.add(() -> executor.execute( + () -> listener.accept(/*presentationState*/true))); + } + } + for (int id : outOfTrustedStateListenerIds) { + if (listenerId == id) { + firedListeners.add(() -> executor.execute( + () -> listener.accept(/*presentationState*/false))); + } + } + }); + } + for (int i = 0; i < firedListeners.size(); i++) { + firedListeners.get(i).run(); + } + } + } + /** @hide */ public void addWindowlessRoot(ViewRootImpl impl) { synchronized (mLock) { @@ -801,7 +889,7 @@ public final class WindowManagerGlobal { public void removeWindowlessRoot(ViewRootImpl impl) { synchronized (mLock) { mWindowlessRoots.remove(impl); - } + } } public void setRecentsAppBehindSystemBars(boolean behindSystemBars) { diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java index d7b74b3bcfe2..b4b1fde89a46 100644 --- a/core/java/android/view/WindowManagerImpl.java +++ b/core/java/android/view/WindowManagerImpl.java @@ -37,6 +37,7 @@ import android.os.StrictMode; import android.util.Log; import android.window.ITaskFpsCallback; import android.window.TaskFpsCallback; +import android.window.TrustedPresentationThresholds; import android.window.WindowContext; import android.window.WindowMetricsController; import android.window.WindowProvider; @@ -508,4 +509,17 @@ public final class WindowManagerImpl implements WindowManager { } return false; } + + @Override + public void registerTrustedPresentationListener(@NonNull IBinder window, + @NonNull TrustedPresentationThresholds thresholds, @NonNull Executor executor, + @NonNull Consumer listener) { + mGlobal.registerTrustedPresentationListener(window, thresholds, executor, listener); + } + + @Override + public void unregisterTrustedPresentationListener(@NonNull Consumer listener) { + mGlobal.unregisterTrustedPresentationListener(listener); + + } } diff --git a/core/java/android/window/ITrustedPresentationListener.aidl b/core/java/android/window/ITrustedPresentationListener.aidl new file mode 100644 index 000000000000..b33128abb7e5 --- /dev/null +++ b/core/java/android/window/ITrustedPresentationListener.aidl @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.window; + +/** + * @hide + */ +oneway interface ITrustedPresentationListener { + void onTrustedPresentationChanged(in int[] enteredTrustedStateIds, in int[] exitedTrustedStateIds); +} \ No newline at end of file diff --git a/core/java/android/window/TrustedPresentationListener.java b/core/java/android/window/TrustedPresentationListener.java new file mode 100644 index 000000000000..02fd6d98fb0d --- /dev/null +++ b/core/java/android/window/TrustedPresentationListener.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.window; + +/** + * @hide + */ +public interface TrustedPresentationListener { + + void onTrustedPresentationChanged(boolean inTrustedPresentationState); + +} diff --git a/core/java/android/window/TrustedPresentationThresholds.aidl b/core/java/android/window/TrustedPresentationThresholds.aidl new file mode 100644 index 000000000000..d7088bf0fddc --- /dev/null +++ b/core/java/android/window/TrustedPresentationThresholds.aidl @@ -0,0 +1,3 @@ +package android.window; + +parcelable TrustedPresentationThresholds; diff --git a/core/java/android/window/TrustedPresentationThresholds.java b/core/java/android/window/TrustedPresentationThresholds.java new file mode 100644 index 000000000000..801d35c49228 --- /dev/null +++ b/core/java/android/window/TrustedPresentationThresholds.java @@ -0,0 +1,127 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.window; + +import android.annotation.FloatRange; +import android.annotation.IntRange; +import android.os.Parcel; +import android.os.Parcelable; +import android.view.SurfaceControl; + +import androidx.annotation.NonNull; + +/** + * @hide + */ +public final class TrustedPresentationThresholds implements Parcelable { + /** + * The min alpha the {@link SurfaceControl} is required to have to be considered inside the + * threshold. + */ + @FloatRange(from = 0f, fromInclusive = false, to = 1f) + public final float mMinAlpha; + + /** + * The min fraction of the SurfaceControl that was presented to the user to be considered + * inside the threshold. + */ + @FloatRange(from = 0f, fromInclusive = false, to = 1f) + public final float mMinFractionRendered; + + /** + * The time in milliseconds required for the {@link SurfaceControl} to be in the threshold. + */ + @IntRange(from = 1) + public final int mStabilityRequirementMs; + + private void checkValid() { + if (mMinAlpha <= 0 || mMinFractionRendered <= 0 || mStabilityRequirementMs < 1) { + throw new IllegalArgumentException( + "TrustedPresentationThresholds values are invalid"); + } + } + + /** + * Creates a new TrustedPresentationThresholds. + * + * @param minAlpha The min alpha the {@link SurfaceControl} is required to + * have to be considered inside the + * threshold. + * @param minFractionRendered The min fraction of the SurfaceControl that was presented + * to the user to be considered + * inside the threshold. + * @param stabilityRequirementMs The time in milliseconds required for the + * {@link SurfaceControl} to be in the threshold. + */ + public TrustedPresentationThresholds( + @FloatRange(from = 0f, fromInclusive = false, to = 1f) float minAlpha, + @FloatRange(from = 0f, fromInclusive = false, to = 1f) float minFractionRendered, + @IntRange(from = 1) int stabilityRequirementMs) { + this.mMinAlpha = minAlpha; + this.mMinFractionRendered = minFractionRendered; + this.mStabilityRequirementMs = stabilityRequirementMs; + checkValid(); + } + + @Override + public String toString() { + return "TrustedPresentationThresholds { " + + "minAlpha = " + mMinAlpha + ", " + + "minFractionRendered = " + mMinFractionRendered + ", " + + "stabilityRequirementMs = " + mStabilityRequirementMs + + " }"; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeFloat(mMinAlpha); + dest.writeFloat(mMinFractionRendered); + dest.writeInt(mStabilityRequirementMs); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * @hide + */ + TrustedPresentationThresholds(@NonNull Parcel in) { + mMinAlpha = in.readFloat(); + mMinFractionRendered = in.readFloat(); + mStabilityRequirementMs = in.readInt(); + + checkValid(); + } + + /** + * @hide + */ + public static final @NonNull Creator CREATOR = + new Creator() { + @Override + public TrustedPresentationThresholds[] newArray(int size) { + return new TrustedPresentationThresholds[size]; + } + + @Override + public TrustedPresentationThresholds createFromParcel(@NonNull Parcel in) { + return new TrustedPresentationThresholds(in); + } + }; +} diff --git a/core/java/com/android/internal/protolog/ProtoLogGroup.java b/core/java/com/android/internal/protolog/ProtoLogGroup.java index 4bb7c33b41e2..8c2a52560050 100644 --- a/core/java/com/android/internal/protolog/ProtoLogGroup.java +++ b/core/java/com/android/internal/protolog/ProtoLogGroup.java @@ -93,6 +93,7 @@ public enum ProtoLogGroup implements IProtoLogGroup { WM_DEBUG_DREAM(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, Consts.TAG_WM), WM_DEBUG_DIMMER(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM), + WM_DEBUG_TPL(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM), TEST_GROUP(true, true, false, "WindowManagerProtoLogTest"); private final boolean mEnabled; diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 2237ba1924db..19128212094d 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -595,6 +595,12 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "-1518132958": { + "message": "fractionRendered boundsOverSource=%f", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "-1517908912": { "message": "requestScrollCapture: caught exception dispatching to window.token=%s", "level": "WARN", @@ -961,6 +967,12 @@ "group": "WM_DEBUG_CONTENT_RECORDING", "at": "com\/android\/server\/wm\/ContentRecorder.java" }, + "-1209762265": { + "message": "Registering listener=%s with id=%d for window=%s with %s", + "level": "DEBUG", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "-1209252064": { "message": "Clear animatingExit: reason=clearAnimatingFlags win=%s", "level": "DEBUG", @@ -1333,6 +1345,12 @@ "group": "WM_DEBUG_WINDOW_TRANSITIONS", "at": "com\/android\/server\/wm\/Transition.java" }, + "-888703350": { + "message": "Skipping %s", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "-883738232": { "message": "Adding more than one toast window for UID at a time.", "level": "WARN", @@ -2803,6 +2821,12 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "360319850": { + "message": "fractionRendered scale=%f", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "364992694": { "message": "freezeDisplayRotation: current rotation=%d, new rotation=%d, caller=%s", "level": "VERBOSE", @@ -2983,6 +3007,12 @@ "group": "WM_DEBUG_BACK_PREVIEW", "at": "com\/android\/server\/wm\/BackNavigationController.java" }, + "532771960": { + "message": "Adding untrusted state listener=%s with id=%d", + "level": "DEBUG", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "535103992": { "message": "Wallpaper may change! Adjusting", "level": "VERBOSE", @@ -3061,6 +3091,12 @@ "group": "WM_DEBUG_DREAM", "at": "com\/android\/server\/wm\/ActivityTaskManagerService.java" }, + "605179032": { + "message": "checkIfInThreshold fractionRendered=%f alpha=%f currTimeMs=%d", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "608694300": { "message": " NEW SURFACE SESSION %s", "level": "INFO", @@ -3289,6 +3325,12 @@ "group": "WM_SHOW_TRANSACTIONS", "at": "com\/android\/server\/wm\/WindowState.java" }, + "824532141": { + "message": "lastState=%s newState=%s alpha=%f minAlpha=%f fractionRendered=%f minFractionRendered=%f", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "829434921": { "message": "Draw state now committed in %s", "level": "VERBOSE", @@ -3583,6 +3625,12 @@ "group": "WM_SHOW_SURFACE_ALLOC", "at": "com\/android\/server\/wm\/ScreenRotationAnimation.java" }, + "1090378847": { + "message": "Checking %d windows", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "1100065297": { "message": "Attempted to get IME policy of a display that does not exist: %d", "level": "WARN", @@ -3715,6 +3763,12 @@ "group": "WM_DEBUG_FOCUS", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "1251721200": { + "message": "unregister failed, couldn't find deathRecipient for %s with id=%d", + "level": "ERROR", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "1252594551": { "message": "Window types in WindowContext and LayoutParams.type should match! Type from LayoutParams is %d, but type from WindowContext is %d", "level": "WARN", @@ -3853,6 +3907,12 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/TaskDisplayArea.java" }, + "1382634842": { + "message": "Unregistering listener=%s with id=%d", + "level": "DEBUG", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "1393721079": { "message": "Starting remote display change: from [rot = %d], to [%dx%d, rot = %d]", "level": "VERBOSE", @@ -3901,6 +3961,12 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "1445704347": { + "message": "coveredRegionsAbove updated with %s frame:%s region:%s", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "1448683958": { "message": "Override pending remote transitionSet=%b adapter=%s", "level": "INFO", @@ -4201,6 +4267,12 @@ "group": "WM_DEBUG_RECENTS_ANIMATIONS", "at": "com\/android\/server\/wm\/RecentsAnimation.java" }, + "1786463281": { + "message": "Adding trusted state listener=%s with id=%d", + "level": "DEBUG", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "1789321832": { "message": "Then token:%s is invalid. It might be removed", "level": "WARN", @@ -4375,6 +4447,12 @@ "group": "WM_DEBUG_TASKS", "at": "com\/android\/server\/wm\/RootWindowContainer.java" }, + "1955470028": { + "message": "computeFractionRendered: visibleRegion=%s screenBounds=%s contentSize=%s scale=%f,%f", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "1964565370": { "message": "Starting remote animation", "level": "INFO", @@ -4659,6 +4737,9 @@ "WM_DEBUG_TASKS": { "tag": "WindowManager" }, + "WM_DEBUG_TPL": { + "tag": "WindowManager" + }, "WM_DEBUG_WALLPAPER": { "tag": "WindowManager" }, diff --git a/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java b/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java new file mode 100644 index 000000000000..e82dc37c2b6a --- /dev/null +++ b/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java @@ -0,0 +1,448 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.graphics.Matrix.MSCALE_X; +import static android.graphics.Matrix.MSCALE_Y; +import static android.graphics.Matrix.MSKEW_X; +import static android.graphics.Matrix.MSKEW_Y; + +import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TPL; + +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.IntArray; +import android.util.Pair; +import android.util.Size; +import android.view.InputWindowHandle; +import android.window.ITrustedPresentationListener; +import android.window.TrustedPresentationThresholds; +import android.window.WindowInfosListener; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.server.wm.utils.RegionUtils; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Optional; + +/** + * Class to handle TrustedPresentationListener registrations in a thread safe manner. This class + * also takes care of cleaning up listeners when the remote process dies. + */ +public class TrustedPresentationListenerController { + + // Should only be accessed by the posting to the handler + private class Listeners { + private final class ListenerDeathRecipient implements IBinder.DeathRecipient { + IBinder mListenerBinder; + int mInstances; + + ListenerDeathRecipient(IBinder listenerBinder) { + mListenerBinder = listenerBinder; + mInstances = 0; + try { + mListenerBinder.linkToDeath(this, 0); + } catch (RemoteException ignore) { + } + } + + void addInstance() { + mInstances++; + } + + // return true if there are no instances alive + boolean removeInstance() { + mInstances--; + if (mInstances > 0) { + return false; + } + mListenerBinder.unlinkToDeath(this, 0); + return true; + } + + public void binderDied() { + mHandler.post(() -> { + mUniqueListeners.remove(mListenerBinder); + removeListeners(mListenerBinder, Optional.empty()); + }); + } + } + + // tracks binder deaths for cleanup + ArrayMap mUniqueListeners = new ArrayMap<>(); + ArrayMap> mWindowToListeners = + new ArrayMap<>(); + + void register(IBinder window, ITrustedPresentationListener listener, + TrustedPresentationThresholds thresholds, int id) { + var listenersForWindow = mWindowToListeners.computeIfAbsent(window, + iBinder -> new ArrayList<>()); + listenersForWindow.add(new TrustedPresentationInfo(thresholds, id, listener)); + + // register death listener + var listenerBinder = listener.asBinder(); + var deathRecipient = mUniqueListeners.computeIfAbsent(listenerBinder, + ListenerDeathRecipient::new); + deathRecipient.addInstance(); + } + + void unregister(ITrustedPresentationListener trustedPresentationListener, int id) { + var listenerBinder = trustedPresentationListener.asBinder(); + var deathRecipient = mUniqueListeners.get(listenerBinder); + if (deathRecipient == null) { + ProtoLog.e(WM_DEBUG_TPL, "unregister failed, couldn't find" + + " deathRecipient for %s with id=%d", trustedPresentationListener, id); + return; + } + + if (deathRecipient.removeInstance()) { + mUniqueListeners.remove(listenerBinder); + } + removeListeners(listenerBinder, Optional.of(id)); + } + + boolean isEmpty() { + return mWindowToListeners.isEmpty(); + } + + ArrayList get(IBinder windowToken) { + return mWindowToListeners.get(windowToken); + } + + private void removeListeners(IBinder listenerBinder, Optional id) { + for (int i = mWindowToListeners.size() - 1; i >= 0; i--) { + var listeners = mWindowToListeners.valueAt(i); + for (int j = listeners.size() - 1; j >= 0; j--) { + var listener = listeners.get(j); + if (listener.mListener.asBinder() == listenerBinder && (id.isEmpty() + || listener.mId == id.get())) { + listeners.remove(j); + } + } + if (listeners.isEmpty()) { + mWindowToListeners.removeAt(i); + } + } + } + } + + private final Object mHandlerThreadLock = new Object(); + private HandlerThread mHandlerThread; + private Handler mHandler; + + private WindowInfosListener mWindowInfosListener; + + Listeners mRegisteredListeners = new Listeners(); + + private InputWindowHandle[] mLastWindowHandles; + + private final Object mIgnoredWindowTokensLock = new Object(); + + private final ArraySet mIgnoredWindowTokens = new ArraySet<>(); + + private void startHandlerThreadIfNeeded() { + synchronized (mHandlerThreadLock) { + if (mHandler == null) { + mHandlerThread = new HandlerThread("WindowInfosListenerForTpl"); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); + } + } + } + + void addIgnoredWindowTokens(IBinder token) { + synchronized (mIgnoredWindowTokensLock) { + mIgnoredWindowTokens.add(token); + } + } + + void removeIgnoredWindowTokens(IBinder token) { + synchronized (mIgnoredWindowTokensLock) { + mIgnoredWindowTokens.remove(token); + } + } + + void registerListener(IBinder window, ITrustedPresentationListener listener, + TrustedPresentationThresholds thresholds, int id) { + startHandlerThreadIfNeeded(); + mHandler.post(() -> { + ProtoLog.d(WM_DEBUG_TPL, "Registering listener=%s with id=%d for window=%s with %s", + listener, id, window, thresholds); + + mRegisteredListeners.register(window, listener, thresholds, id); + registerWindowInfosListener(); + // Update the initial state for the new registered listener + computeTpl(mLastWindowHandles); + }); + } + + void unregisterListener(ITrustedPresentationListener listener, int id) { + startHandlerThreadIfNeeded(); + mHandler.post(() -> { + ProtoLog.d(WM_DEBUG_TPL, "Unregistering listener=%s with id=%d", + listener, id); + + mRegisteredListeners.unregister(listener, id); + if (mRegisteredListeners.isEmpty()) { + unregisterWindowInfosListener(); + } + }); + } + + void dump(PrintWriter pw) { + final String innerPrefix = " "; + pw.println("TrustedPresentationListenerController:"); + pw.println(innerPrefix + "Active unique listeners (" + + mRegisteredListeners.mUniqueListeners.size() + "):"); + for (int i = 0; i < mRegisteredListeners.mWindowToListeners.size(); i++) { + pw.println( + innerPrefix + " window=" + mRegisteredListeners.mWindowToListeners.keyAt(i)); + final var listeners = mRegisteredListeners.mWindowToListeners.valueAt(i); + for (int j = 0; j < listeners.size(); j++) { + final var listener = listeners.get(j); + pw.println(innerPrefix + innerPrefix + " listener=" + listener.mListener.asBinder() + + " id=" + listener.mId + + " thresholds=" + listener.mThresholds); + } + } + } + + private void registerWindowInfosListener() { + if (mWindowInfosListener != null) { + return; + } + + mWindowInfosListener = new WindowInfosListener() { + @Override + public void onWindowInfosChanged(InputWindowHandle[] windowHandles, + DisplayInfo[] displayInfos) { + mHandler.post(() -> computeTpl(windowHandles)); + } + }; + mLastWindowHandles = mWindowInfosListener.register().first; + } + + private void unregisterWindowInfosListener() { + if (mWindowInfosListener == null) { + return; + } + + mWindowInfosListener.unregister(); + mWindowInfosListener = null; + mLastWindowHandles = null; + } + + private void computeTpl(InputWindowHandle[] windowHandles) { + mLastWindowHandles = windowHandles; + if (mLastWindowHandles == null || mLastWindowHandles.length == 0 + || mRegisteredListeners.isEmpty()) { + return; + } + + Rect tmpRect = new Rect(); + Matrix tmpInverseMatrix = new Matrix(); + float[] tmpMatrix = new float[9]; + Region coveredRegionsAbove = new Region(); + long currTimeMs = System.currentTimeMillis(); + ProtoLog.v(WM_DEBUG_TPL, "Checking %d windows", mLastWindowHandles.length); + + ArrayMap> listenerUpdates = + new ArrayMap<>(); + ArraySet ignoredWindowTokens; + synchronized (mIgnoredWindowTokensLock) { + ignoredWindowTokens = new ArraySet<>(mIgnoredWindowTokens); + } + for (var windowHandle : mLastWindowHandles) { + if (ignoredWindowTokens.contains(windowHandle.getWindowToken())) { + ProtoLog.v(WM_DEBUG_TPL, "Skipping %s", windowHandle.name); + continue; + } + tmpRect.set(windowHandle.frame); + var listeners = mRegisteredListeners.get(windowHandle.getWindowToken()); + if (listeners != null) { + Region region = new Region(); + region.op(tmpRect, coveredRegionsAbove, Region.Op.DIFFERENCE); + windowHandle.transform.invert(tmpInverseMatrix); + tmpInverseMatrix.getValues(tmpMatrix); + float scaleX = (float) Math.sqrt(tmpMatrix[MSCALE_X] * tmpMatrix[MSCALE_X] + + tmpMatrix[MSKEW_X] * tmpMatrix[MSKEW_X]); + float scaleY = (float) Math.sqrt(tmpMatrix[MSCALE_Y] * tmpMatrix[MSCALE_Y] + + tmpMatrix[MSKEW_Y] * tmpMatrix[MSKEW_Y]); + + float fractionRendered = computeFractionRendered(region, new RectF(tmpRect), + windowHandle.contentSize, + scaleX, scaleY); + + checkIfInThreshold(listeners, listenerUpdates, fractionRendered, windowHandle.alpha, + currTimeMs); + } + + coveredRegionsAbove.op(tmpRect, Region.Op.UNION); + ProtoLog.v(WM_DEBUG_TPL, "coveredRegionsAbove updated with %s frame:%s region:%s", + windowHandle.name, tmpRect.toShortString(), coveredRegionsAbove); + } + + for (int i = 0; i < listenerUpdates.size(); i++) { + var updates = listenerUpdates.valueAt(i); + var listener = listenerUpdates.keyAt(i); + try { + listener.onTrustedPresentationChanged(updates.first.toArray(), + updates.second.toArray()); + } catch (RemoteException ignore) { + } + } + } + + private void addListenerUpdate( + ArrayMap> listenerUpdates, + ITrustedPresentationListener listener, int id, boolean presentationState) { + var updates = listenerUpdates.get(listener); + if (updates == null) { + updates = new Pair<>(new IntArray(), new IntArray()); + listenerUpdates.put(listener, updates); + } + if (presentationState) { + updates.first.add(id); + } else { + updates.second.add(id); + } + } + + + private void checkIfInThreshold( + ArrayList listeners, + ArrayMap> listenerUpdates, + float fractionRendered, float alpha, long currTimeMs) { + ProtoLog.v(WM_DEBUG_TPL, "checkIfInThreshold fractionRendered=%f alpha=%f currTimeMs=%d", + fractionRendered, alpha, currTimeMs); + for (int i = 0; i < listeners.size(); i++) { + var trustedPresentationInfo = listeners.get(i); + var listener = trustedPresentationInfo.mListener; + boolean lastState = trustedPresentationInfo.mLastComputedTrustedPresentationState; + boolean newState = + (alpha >= trustedPresentationInfo.mThresholds.mMinAlpha) && (fractionRendered + >= trustedPresentationInfo.mThresholds.mMinFractionRendered); + trustedPresentationInfo.mLastComputedTrustedPresentationState = newState; + + ProtoLog.v(WM_DEBUG_TPL, + "lastState=%s newState=%s alpha=%f minAlpha=%f fractionRendered=%f " + + "minFractionRendered=%f", + lastState, newState, alpha, trustedPresentationInfo.mThresholds.mMinAlpha, + fractionRendered, trustedPresentationInfo.mThresholds.mMinFractionRendered); + + if (lastState && !newState) { + // We were in the trusted presentation state, but now we left it, + // emit the callback if needed + if (trustedPresentationInfo.mLastReportedTrustedPresentationState) { + trustedPresentationInfo.mLastReportedTrustedPresentationState = false; + addListenerUpdate(listenerUpdates, listener, + trustedPresentationInfo.mId, /*presentationState*/ false); + ProtoLog.d(WM_DEBUG_TPL, "Adding untrusted state listener=%s with id=%d", + listener, trustedPresentationInfo.mId); + } + // Reset the timer + trustedPresentationInfo.mEnteredTrustedPresentationStateTime = -1; + } else if (!lastState && newState) { + // We were not in the trusted presentation state, but we entered it, begin the timer + // and make sure this gets called at least once more! + trustedPresentationInfo.mEnteredTrustedPresentationStateTime = currTimeMs; + mHandler.postDelayed(() -> { + computeTpl(mLastWindowHandles); + }, (long) (trustedPresentationInfo.mThresholds.mStabilityRequirementMs * 1.5)); + } + + // Has the timer elapsed, but we are still in the state? Emit a callback if needed + if (!trustedPresentationInfo.mLastReportedTrustedPresentationState && newState && ( + currTimeMs - trustedPresentationInfo.mEnteredTrustedPresentationStateTime + > trustedPresentationInfo.mThresholds.mStabilityRequirementMs)) { + trustedPresentationInfo.mLastReportedTrustedPresentationState = true; + addListenerUpdate(listenerUpdates, listener, + trustedPresentationInfo.mId, /*presentationState*/ true); + ProtoLog.d(WM_DEBUG_TPL, "Adding trusted state listener=%s with id=%d", + listener, trustedPresentationInfo.mId); + } + } + } + + private float computeFractionRendered(Region visibleRegion, RectF screenBounds, + Size contentSize, + float sx, float sy) { + ProtoLog.v(WM_DEBUG_TPL, + "computeFractionRendered: visibleRegion=%s screenBounds=%s contentSize=%s " + + "scale=%f,%f", + visibleRegion, screenBounds, contentSize, sx, sy); + + if (contentSize.getWidth() == 0 || contentSize.getHeight() == 0) { + return -1; + } + if (screenBounds.width() == 0 || screenBounds.height() == 0) { + return -1; + } + + float fractionRendered = Math.min(sx * sy, 1.0f); + ProtoLog.v(WM_DEBUG_TPL, "fractionRendered scale=%f", fractionRendered); + + float boundsOverSourceW = screenBounds.width() / (float) contentSize.getWidth(); + float boundsOverSourceH = screenBounds.height() / (float) contentSize.getHeight(); + fractionRendered *= boundsOverSourceW * boundsOverSourceH; + ProtoLog.v(WM_DEBUG_TPL, "fractionRendered boundsOverSource=%f", fractionRendered); + // Compute the size of all the rects since they may be disconnected. + float[] visibleSize = new float[1]; + RegionUtils.forEachRect(visibleRegion, rect -> { + float size = rect.width() * rect.height(); + visibleSize[0] += size; + }); + + fractionRendered *= visibleSize[0] / (screenBounds.width() * screenBounds.height()); + return fractionRendered; + } + + private static class TrustedPresentationInfo { + boolean mLastComputedTrustedPresentationState = false; + boolean mLastReportedTrustedPresentationState = false; + long mEnteredTrustedPresentationStateTime = -1; + final TrustedPresentationThresholds mThresholds; + + final ITrustedPresentationListener mListener; + final int mId; + + private TrustedPresentationInfo(TrustedPresentationThresholds thresholds, int id, + ITrustedPresentationListener listener) { + mThresholds = thresholds; + mId = id; + mListener = listener; + checkValid(thresholds); + } + + private void checkValid(TrustedPresentationThresholds thresholds) { + if (thresholds.mMinAlpha <= 0 || thresholds.mMinFractionRendered <= 0 + || thresholds.mStabilityRequirementMs < 1) { + throw new IllegalArgumentException( + "TrustedPresentationThresholds values are invalid"); + } + } + } +} diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index dd2b48bb5a3d..eaed3f94c5f5 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -303,9 +303,11 @@ import android.window.AddToSurfaceSyncGroupResult; import android.window.ClientWindowFrames; import android.window.ISurfaceSyncGroupCompletedListener; import android.window.ITaskFpsCallback; +import android.window.ITrustedPresentationListener; import android.window.ScreenCapture; import android.window.SystemPerformanceHinter; import android.window.TaskSnapshot; +import android.window.TrustedPresentationThresholds; import android.window.WindowContainerToken; import android.window.WindowContextInfo; @@ -764,6 +766,9 @@ public class WindowManagerService extends IWindowManager.Stub private final SurfaceSyncGroupController mSurfaceSyncGroupController = new SurfaceSyncGroupController(); + final TrustedPresentationListenerController mTrustedPresentationListenerController = + new TrustedPresentationListenerController(); + @VisibleForTesting final class SettingsObserver extends ContentObserver { private final Uri mDisplayInversionEnabledUri = @@ -7171,6 +7176,7 @@ public class WindowManagerService extends IWindowManager.Stub pw.println(separator); } mSystemPerformanceHinter.dump(pw, ""); + mTrustedPresentationListenerController.dump(pw); } } @@ -9762,4 +9768,17 @@ public class WindowManagerService extends IWindowManager.Stub Binder.restoreCallingIdentity(origId); } } + + @Override + public void registerTrustedPresentationListener(IBinder window, + ITrustedPresentationListener listener, + TrustedPresentationThresholds thresholds, int id) { + mTrustedPresentationListenerController.registerListener(window, listener, thresholds, id); + } + + @Override + public void unregisterTrustedPresentationListener(ITrustedPresentationListener listener, + int id) { + mTrustedPresentationListenerController.unregisterListener(listener, id); + } } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index e1f1f662c5aa..7bc7e2cb780b 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -1189,6 +1189,11 @@ class WindowState extends WindowContainer implements WindowManagerP ProtoLog.v(WM_DEBUG_ADD_REMOVE, "Adding %s to %s", this, parentWindow); parentWindow.addChild(this, sWindowSubLayerComparator); } + + if (token.mRoundedCornerOverlay) { + mWmService.mTrustedPresentationListenerController.addIgnoredWindowTokens( + getWindowToken()); + } } @Override @@ -2393,6 +2398,9 @@ class WindowState extends WindowContainer implements WindowManagerP } mWmService.postWindowRemoveCleanupLocked(this); + + mWmService.mTrustedPresentationListenerController.removeIgnoredWindowTokens( + getWindowToken()); } @Override diff --git a/services/tests/wmtests/AndroidManifest.xml b/services/tests/wmtests/AndroidManifest.xml index c3074bb0fee8..a8d3fa110844 100644 --- a/services/tests/wmtests/AndroidManifest.xml +++ b/services/tests/wmtests/AndroidManifest.xml @@ -99,7 +99,7 @@ android:theme="@style/WhiteBackgroundTheme" android:exported="true"/> - diff --git a/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationCallbackTest.java b/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationCallbackTest.java deleted file mode 100644 index c5dd447b5b0c..000000000000 --- a/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationCallbackTest.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm; - -import static android.server.wm.ActivityManagerTestBase.createFullscreenActivityScenarioRule; -import static android.server.wm.BuildUtils.HW_TIMEOUT_MULTIPLIER; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import android.app.Activity; -import android.platform.test.annotations.Presubmit; -import android.server.wm.CtsWindowInfoUtils; -import android.view.SurfaceControl; -import android.view.SurfaceControl.TrustedPresentationThresholds; - -import androidx.annotation.GuardedBy; -import androidx.test.ext.junit.rules.ActivityScenarioRule; - -import com.android.server.wm.utils.CommonUtils; - -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TestName; - -import java.util.function.Consumer; - -/** - * TODO (b/287076178): Move these tests to - * {@link android.view.surfacecontrol.cts.TrustedPresentationCallbackTest} when API is made public - */ -@Presubmit -public class TrustedPresentationCallbackTest { - private static final String TAG = "TrustedPresentationCallbackTest"; - private static final int STABILITY_REQUIREMENT_MS = 500; - private static final long WAIT_TIME_MS = HW_TIMEOUT_MULTIPLIER * 2000L; - - private static final float FRACTION_VISIBLE = 0.1f; - - private final Object mResultsLock = new Object(); - @GuardedBy("mResultsLock") - private boolean mResult; - @GuardedBy("mResultsLock") - private boolean mReceivedResults; - - @Rule - public TestName mName = new TestName(); - - @Rule - public ActivityScenarioRule mActivityRule = createFullscreenActivityScenarioRule( - TestActivity.class); - - private TestActivity mActivity; - - @Before - public void setup() { - mActivityRule.getScenario().onActivity(activity -> mActivity = activity); - } - - @After - public void tearDown() { - CommonUtils.waitUntilActivityRemoved(mActivity); - } - - @Test - public void testAddTrustedPresentationListenerOnWindow() throws InterruptedException { - TrustedPresentationThresholds thresholds = new TrustedPresentationThresholds( - 1 /* minAlpha */, FRACTION_VISIBLE, STABILITY_REQUIREMENT_MS); - SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - mActivity.getWindow().getRootSurfaceControl().addTrustedPresentationCallback(t, thresholds, - Runnable::run, inTrustedPresentationState -> { - synchronized (mResultsLock) { - mResult = inTrustedPresentationState; - mReceivedResults = true; - mResultsLock.notify(); - } - }); - t.apply(); - synchronized (mResultsLock) { - assertResults(); - } - } - - @Test - public void testRemoveTrustedPresentationListenerOnWindow() throws InterruptedException { - TrustedPresentationThresholds thresholds = new TrustedPresentationThresholds( - 1 /* minAlpha */, FRACTION_VISIBLE, STABILITY_REQUIREMENT_MS); - Consumer trustedPresentationCallback = inTrustedPresentationState -> { - synchronized (mResultsLock) { - mResult = inTrustedPresentationState; - mReceivedResults = true; - mResultsLock.notify(); - } - }; - SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - mActivity.getWindow().getRootSurfaceControl().addTrustedPresentationCallback(t, thresholds, - Runnable::run, trustedPresentationCallback); - t.apply(); - - synchronized (mResultsLock) { - if (!mReceivedResults) { - mResultsLock.wait(WAIT_TIME_MS); - } - assertResults(); - // reset the state - mReceivedResults = false; - } - - mActivity.getWindow().getRootSurfaceControl().removeTrustedPresentationCallback(t, - trustedPresentationCallback); - t.apply(); - - synchronized (mResultsLock) { - if (!mReceivedResults) { - mResultsLock.wait(WAIT_TIME_MS); - } - // Ensure we waited the full time and never received a notify on the result from the - // callback. - assertFalse("Should never have received a callback", mReceivedResults); - // results shouldn't have changed. - assertTrue(mResult); - } - } - - @GuardedBy("mResultsLock") - private void assertResults() throws InterruptedException { - mResultsLock.wait(WAIT_TIME_MS); - - if (!mReceivedResults) { - CtsWindowInfoUtils.dumpWindowsOnScreen(TAG, "test " + mName.getMethodName()); - } - // Make sure we received the results and not just timed out - assertTrue("Timed out waiting for results", mReceivedResults); - assertTrue(mResult); - } - - public static class TestActivity extends Activity { - } -} diff --git a/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationListenerTest.java b/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationListenerTest.java new file mode 100644 index 000000000000..96b66bfd3bc0 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationListenerTest.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.server.wm.ActivityManagerTestBase.createFullscreenActivityScenarioRule; +import static android.server.wm.BuildUtils.HW_TIMEOUT_MULTIPLIER; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; + +import android.app.Activity; +import android.os.SystemClock; +import android.platform.test.annotations.Presubmit; +import android.server.wm.CtsWindowInfoUtils; +import android.util.AndroidRuntimeException; +import android.util.Log; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.WindowManager; +import android.window.TrustedPresentationThresholds; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.test.ext.junit.rules.ActivityScenarioRule; + +import com.android.server.wm.utils.CommonUtils; + +import junit.framework.Assert; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * TODO (b/287076178): Move these tests to + * {@link android.view.surfacecontrol.cts.TrustedPresentationListenerTest} when API is made public + */ +@Presubmit +public class TrustedPresentationListenerTest { + private static final String TAG = "TrustedPresentationListenerTest"; + private static final int STABILITY_REQUIREMENT_MS = 500; + private static final long WAIT_TIME_MS = HW_TIMEOUT_MULTIPLIER * 2000L; + + private static final float FRACTION_VISIBLE = 0.1f; + + private final List mResults = Collections.synchronizedList(new ArrayList<>()); + private CountDownLatch mReceivedResults = new CountDownLatch(1); + + private TrustedPresentationThresholds mThresholds = new TrustedPresentationThresholds( + 1 /* minAlpha */, FRACTION_VISIBLE, STABILITY_REQUIREMENT_MS); + + @Rule + public TestName mName = new TestName(); + + @Rule + public ActivityScenarioRule mActivityRule = createFullscreenActivityScenarioRule( + TestActivity.class); + + private TestActivity mActivity; + + private SurfaceControlViewHost.SurfacePackage mSurfacePackage = null; + + @Before + public void setup() { + mActivityRule.getScenario().onActivity(activity -> mActivity = activity); + mDefaultListener = new Listener(mReceivedResults); + } + + @After + public void tearDown() { + if (mSurfacePackage != null) { + new SurfaceControl.Transaction().remove(mSurfacePackage.getSurfaceControl()).apply( + true); + mSurfacePackage.release(); + } + CommonUtils.waitUntilActivityRemoved(mActivity); + + } + + private class Listener implements Consumer { + final CountDownLatch mLatch; + + Listener(CountDownLatch latch) { + mLatch = latch; + } + + @Override + public void accept(Boolean inTrustedPresentationState) { + Log.d(TAG, "onTrustedPresentationChanged " + inTrustedPresentationState); + mResults.add(inTrustedPresentationState); + mLatch.countDown(); + } + } + + private Consumer mDefaultListener; + + @Test + public void testAddTrustedPresentationListenerOnWindow() { + WindowManager windowManager = mActivity.getSystemService(WindowManager.class); + windowManager.registerTrustedPresentationListener( + mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, Runnable::run, + mDefaultListener); + assertResults(List.of(true)); + } + + @Test + public void testRemoveTrustedPresentationListenerOnWindow() throws InterruptedException { + WindowManager windowManager = mActivity.getSystemService(WindowManager.class); + windowManager.registerTrustedPresentationListener( + mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, Runnable::run, + mDefaultListener); + assertResults(List.of(true)); + // reset the latch + mReceivedResults = new CountDownLatch(1); + + windowManager.unregisterTrustedPresentationListener(mDefaultListener); + mReceivedResults.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS); + // Ensure we waited the full time and never received a notify on the result from the + // callback. + assertEquals("Should never have received a callback", mReceivedResults.getCount(), 1); + // results shouldn't have changed. + assertEquals(mResults, List.of(true)); + } + + @Test + public void testRemovingUnknownListenerIsANoop() { + WindowManager windowManager = mActivity.getSystemService(WindowManager.class); + assertNotNull(windowManager); + windowManager.unregisterTrustedPresentationListener(mDefaultListener); + } + + @Test + public void testAddDuplicateListenerThrowsException() { + WindowManager windowManager = mActivity.getSystemService(WindowManager.class); + assertNotNull(windowManager); + windowManager.registerTrustedPresentationListener( + mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, + Runnable::run, mDefaultListener); + assertThrows(AndroidRuntimeException.class, + () -> windowManager.registerTrustedPresentationListener( + mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, + Runnable::run, mDefaultListener)); + } + + @Test + public void testAddDuplicateThresholds() { + mReceivedResults = new CountDownLatch(2); + mDefaultListener = new Listener(mReceivedResults); + WindowManager windowManager = mActivity.getSystemService(WindowManager.class); + windowManager.registerTrustedPresentationListener( + mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, + Runnable::run, mDefaultListener); + + Consumer mNewListener = new Listener(mReceivedResults); + + windowManager.registerTrustedPresentationListener( + mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, + Runnable::run, mNewListener); + assertResults(List.of(true, true)); + } + + private void waitForViewAttach(View view) { + final CountDownLatch viewAttached = new CountDownLatch(1); + view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(@NonNull View v) { + viewAttached.countDown(); + } + + @Override + public void onViewDetachedFromWindow(@NonNull View v) { + + } + }); + try { + viewAttached.await(2000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + if (!wait(viewAttached, 2000 /* waitTimeMs */)) { + fail("Couldn't attach view=" + view); + } + } + + @Test + public void testAddListenerToScvh() { + WindowManager windowManager = mActivity.getSystemService(WindowManager.class); + + var embeddedView = new View(mActivity); + mActivityRule.getScenario().onActivity(activity -> { + var attachedSurfaceControl = + mActivity.getWindow().getDecorView().getRootSurfaceControl(); + var scvh = new SurfaceControlViewHost(mActivity, mActivity.getDisplay(), + attachedSurfaceControl.getHostToken()); + mSurfacePackage = scvh.getSurfacePackage(); + scvh.setView(embeddedView, mActivity.getWindow().getDecorView().getWidth(), + mActivity.getWindow().getDecorView().getHeight()); + attachedSurfaceControl.buildReparentTransaction( + mSurfacePackage.getSurfaceControl()); + }); + + waitForViewAttach(embeddedView); + windowManager.registerTrustedPresentationListener(embeddedView.getWindowToken(), + mThresholds, + Runnable::run, mDefaultListener); + + assertResults(List.of(true)); + } + + private boolean wait(CountDownLatch latch, long waitTimeMs) { + while (true) { + long now = SystemClock.uptimeMillis(); + try { + return latch.await(waitTimeMs, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + long elapsedTime = SystemClock.uptimeMillis() - now; + waitTimeMs = Math.max(0, waitTimeMs - elapsedTime); + } + } + + } + + @GuardedBy("mResultsLock") + private void assertResults(List results) { + if (!wait(mReceivedResults, WAIT_TIME_MS)) { + try { + CtsWindowInfoUtils.dumpWindowsOnScreen(TAG, "test " + mName.getMethodName()); + } catch (InterruptedException e) { + Log.d(TAG, "Couldn't dump windows", e); + } + Assert.fail("Timed out waiting for results mReceivedResults.count=" + + mReceivedResults.getCount() + "mReceivedResults=" + mReceivedResults); + } + + // Make sure we received the results + assertEquals(results.toArray(), mResults.toArray()); + } + + public static class TestActivity extends Activity { + } +} -- cgit v1.2.3-59-g8ed1b From f08b6c69fdd239849c753145909d497e9ab703a4 Mon Sep 17 00:00:00 2001 From: Joshua Trask Date: Tue, 21 Nov 2023 16:37:28 +0000 Subject: Grant SHOW_CUSTOMIZED_RESOLVER permission to Shell (This is needed to be able to exercise the new API in a CTS test.) Test: (see other CLs in this topic) Bug: 268089816 Change-Id: I63de51fbb837071fbf99b8bdc9135bcfb3b12b52 --- data/etc/privapp-permissions-platform.xml | 2 ++ packages/Shell/AndroidManifest.xml | 3 +++ 2 files changed, 5 insertions(+) (limited to 'data') diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 1f08955e5f58..3cf28c919f07 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -383,6 +383,8 @@ applications that come with the platform + + diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 650319fd58f9..b6a0c7bafa44 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -442,6 +442,9 @@ + + + -- cgit v1.2.3-59-g8ed1b From c6b20cda93f2dd4da64d4c4a08468d5f3f419b1c Mon Sep 17 00:00:00 2001 From: Vishnu Nair Date: Wed, 6 Dec 2023 00:53:52 +0000 Subject: Revert "Use WindowInfosListener for TPL" This reverts commit ae3a53650fe557f3c7d89dd231aecbde6de4062a. Reason for revert: possible mem regression b/314407848 Change-Id: If2fd10d1ec46d00d3349636cd4e117ebbd038cfc --- core/java/android/view/AttachedSurfaceControl.java | 39 ++ core/java/android/view/IWindowManager.aidl | 8 - core/java/android/view/ViewRootImpl.java | 12 + core/java/android/view/WindowManager.java | 33 -- core/java/android/view/WindowManagerGlobal.java | 94 +---- core/java/android/view/WindowManagerImpl.java | 14 - .../window/ITrustedPresentationListener.aidl | 24 -- .../window/TrustedPresentationListener.java | 26 -- .../window/TrustedPresentationThresholds.aidl | 3 - .../window/TrustedPresentationThresholds.java | 127 ------ .../android/internal/protolog/ProtoLogGroup.java | 1 - data/etc/services.core.protolog.json | 81 ---- .../wm/TrustedPresentationListenerController.java | 448 --------------------- .../android/server/wm/WindowManagerService.java | 19 - .../java/com/android/server/wm/WindowState.java | 8 - services/tests/wmtests/AndroidManifest.xml | 2 +- .../server/wm/TrustedPresentationCallbackTest.java | 154 +++++++ .../server/wm/TrustedPresentationListenerTest.java | 267 ------------ 18 files changed, 209 insertions(+), 1151 deletions(-) delete mode 100644 core/java/android/window/ITrustedPresentationListener.aidl delete mode 100644 core/java/android/window/TrustedPresentationListener.java delete mode 100644 core/java/android/window/TrustedPresentationThresholds.aidl delete mode 100644 core/java/android/window/TrustedPresentationThresholds.java delete mode 100644 services/core/java/com/android/server/wm/TrustedPresentationListenerController.java create mode 100644 services/tests/wmtests/src/com/android/server/wm/TrustedPresentationCallbackTest.java delete mode 100644 services/tests/wmtests/src/com/android/server/wm/TrustedPresentationListenerTest.java (limited to 'data') diff --git a/core/java/android/view/AttachedSurfaceControl.java b/core/java/android/view/AttachedSurfaceControl.java index f28574ecb3b2..fd5517d29d74 100644 --- a/core/java/android/view/AttachedSurfaceControl.java +++ b/core/java/android/view/AttachedSurfaceControl.java @@ -27,6 +27,9 @@ import android.window.SurfaceSyncGroup; import com.android.window.flags.Flags; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + /** * Provides an interface to the root-Surface of a View Hierarchy or Window. This * is used in combination with the {@link android.view.SurfaceControl} API to enable @@ -193,6 +196,42 @@ public interface AttachedSurfaceControl { + "implemented before making this call."); } + /** + * Add a trusted presentation listener on the SurfaceControl associated with this window. + * + * @param t Transaction that the trusted presentation listener is added on. This should + * be applied by the caller. + * @param thresholds The {@link SurfaceControl.TrustedPresentationThresholds} that will specify + * when the to invoke the callback. + * @param executor The {@link Executor} where the callback will be invoked on. + * @param listener The {@link Consumer} that will receive the callbacks when entered or + * exited the threshold. + * + * @see SurfaceControl.Transaction#setTrustedPresentationCallback(SurfaceControl, + * SurfaceControl.TrustedPresentationThresholds, Executor, Consumer) + * + * @hide b/287076178 un-hide with API bump + */ + default void addTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.TrustedPresentationThresholds thresholds, + @NonNull Executor executor, @NonNull Consumer listener) { + } + + /** + * Remove a trusted presentation listener on the SurfaceControl associated with this window. + * + * @param t Transaction that the trusted presentation listener removed on. This should + * be applied by the caller. + * @param listener The {@link Consumer} that was previously registered with + * addTrustedPresentationCallback that should be removed. + * + * @see SurfaceControl.Transaction#clearTrustedPresentationCallback(SurfaceControl) + * @hide b/287076178 un-hide with API bump + */ + default void removeTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t, + @NonNull Consumer listener) { + } + /** * Transfer the currently in progress touch gesture from the host to the requested * {@link SurfaceControlViewHost.SurfacePackage}. This requires that the diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 36b74e39072a..17bbee6d020f 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -73,8 +73,6 @@ import android.window.ISurfaceSyncGroupCompletedListener; import android.window.ITaskFpsCallback; import android.window.ScreenCapture; import android.window.WindowContextInfo; -import android.window.ITrustedPresentationListener; -import android.window.TrustedPresentationThresholds; /** * System private interface to the window manager. @@ -1077,10 +1075,4 @@ interface IWindowManager @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MONITOR_INPUT)") void unregisterDecorViewGestureListener(IDecorViewGestureListener listener, int displayId); - - void registerTrustedPresentationListener(in IBinder window, in ITrustedPresentationListener listener, - in TrustedPresentationThresholds thresholds, int id); - - - void unregisterTrustedPresentationListener(in ITrustedPresentationListener listener, int id); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 8acab2a43823..cac5387116a1 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -11913,6 +11913,18 @@ public final class ViewRootImpl implements ViewParent, scheduleTraversals(); } + @Override + public void addTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.TrustedPresentationThresholds thresholds, + @NonNull Executor executor, @NonNull Consumer listener) { + t.setTrustedPresentationCallback(getSurfaceControl(), thresholds, executor, listener); + } + + @Override + public void removeTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t, + @NonNull Consumer listener) { + t.clearTrustedPresentationCallback(getSurfaceControl()); + } private void logAndTrace(String msg) { if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index f668088e6b44..046ea77f196d 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -122,9 +122,7 @@ import android.view.WindowInsets.Side.InsetsSide; import android.view.WindowInsets.Type; import android.view.WindowInsets.Type.InsetsType; import android.view.accessibility.AccessibilityNodeInfo; -import android.window.ITrustedPresentationListener; import android.window.TaskFpsCallback; -import android.window.TrustedPresentationThresholds; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -5886,35 +5884,4 @@ public interface WindowManager extends ViewManager { default boolean replaceContentOnDisplayWithSc(int displayId, @NonNull SurfaceControl sc) { throw new UnsupportedOperationException(); } - - /** - * Add a trusted presentation listener associated with a window. If the listener has already - * been registered, an AndroidRuntimeException will be thrown. - * - * @param window The Window to add the trusted presentation listener for - * @param thresholds The {@link TrustedPresentationThresholds} that will specify - * when the to invoke the callback. - * @param executor The {@link Executor} where the callback will be invoked on. - * @param listener The {@link ITrustedPresentationListener} that will receive the callbacks - * when entered or exited trusted presentation per the thresholds. - * - * @hide b/287076178 un-hide with API bump - */ - default void registerTrustedPresentationListener(@NonNull IBinder window, - @NonNull TrustedPresentationThresholds thresholds, @NonNull Executor executor, - @NonNull Consumer listener) { - throw new UnsupportedOperationException(); - } - - /** - * Removes a presentation listener associated with a window. If the listener was not previously - * registered, the call will be a noop. - * - * @hide - * @see #registerTrustedPresentationListener(IBinder, - * TrustedPresentationThresholds, Executor, Consumer) - */ - default void unregisterTrustedPresentationListener(@NonNull Consumer listener) { - throw new UnsupportedOperationException(); - } } diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java index a7d814e9ab8c..214f1ec3d1ec 100644 --- a/core/java/android/view/WindowManagerGlobal.java +++ b/core/java/android/view/WindowManagerGlobal.java @@ -30,13 +30,9 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemProperties; import android.util.AndroidRuntimeException; -import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; -import android.util.Pair; import android.view.inputmethod.InputMethodManager; -import android.window.ITrustedPresentationListener; -import android.window.TrustedPresentationThresholds; import com.android.internal.util.FastPrintWriter; @@ -47,7 +43,6 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.WeakHashMap; import java.util.concurrent.Executor; -import java.util.function.Consumer; import java.util.function.IntConsumer; /** @@ -148,9 +143,6 @@ public final class WindowManagerGlobal { private Runnable mSystemPropertyUpdater; - private final TrustedPresentationListener mTrustedPresentationListener = - new TrustedPresentationListener(); - private WindowManagerGlobal() { } @@ -332,7 +324,7 @@ public final class WindowManagerGlobal { final Context context = view.getContext(); if (context != null && (context.getApplicationInfo().flags - & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) { + & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) { wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; } } @@ -490,7 +482,7 @@ public final class WindowManagerGlobal { if (who != null) { WindowLeaked leak = new WindowLeaked( what + " " + who + " has leaked window " - + root.getView() + " that was originally added here"); + + root.getView() + " that was originally added here"); leak.setStackTrace(root.getLocation().getStackTrace()); Log.e(TAG, "", leak); } @@ -798,86 +790,6 @@ public final class WindowManagerGlobal { } } - public void registerTrustedPresentationListener(@NonNull IBinder window, - @NonNull TrustedPresentationThresholds thresholds, Executor executor, - @NonNull Consumer listener) { - mTrustedPresentationListener.addListener(window, thresholds, listener, executor); - } - - public void unregisterTrustedPresentationListener(@NonNull Consumer listener) { - mTrustedPresentationListener.removeListener(listener); - } - - private final class TrustedPresentationListener extends - ITrustedPresentationListener.Stub { - private static int sId = 0; - private final ArrayMap, Pair> mListeners = - new ArrayMap<>(); - - private final Object mTplLock = new Object(); - - private void addListener(IBinder window, TrustedPresentationThresholds thresholds, - Consumer listener, Executor executor) { - synchronized (mTplLock) { - if (mListeners.containsKey(listener)) { - throw new AndroidRuntimeException("Trying to add duplicate listener"); - } - int id = sId++; - mListeners.put(listener, new Pair<>(id, executor)); - try { - WindowManagerGlobal.getWindowManagerService() - .registerTrustedPresentationListener(window, this, thresholds, id); - } catch (RemoteException e) { - e.rethrowAsRuntimeException(); - } - } - } - - private void removeListener(Consumer listener) { - synchronized (mTplLock) { - var removedListener = mListeners.remove(listener); - if (removedListener == null) { - Log.i(TAG, "listener " + listener + " does not exist."); - return; - } - - try { - WindowManagerGlobal.getWindowManagerService() - .unregisterTrustedPresentationListener(this, removedListener.first); - } catch (RemoteException e) { - e.rethrowAsRuntimeException(); - } - } - } - - @Override - public void onTrustedPresentationChanged(int[] inTrustedStateListenerIds, - int[] outOfTrustedStateListenerIds) { - ArrayList firedListeners = new ArrayList<>(); - synchronized (mTplLock) { - mListeners.forEach((listener, idExecutorPair) -> { - final var listenerId = idExecutorPair.first; - final var executor = idExecutorPair.second; - for (int id : inTrustedStateListenerIds) { - if (listenerId == id) { - firedListeners.add(() -> executor.execute( - () -> listener.accept(/*presentationState*/true))); - } - } - for (int id : outOfTrustedStateListenerIds) { - if (listenerId == id) { - firedListeners.add(() -> executor.execute( - () -> listener.accept(/*presentationState*/false))); - } - } - }); - } - for (int i = 0; i < firedListeners.size(); i++) { - firedListeners.get(i).run(); - } - } - } - /** @hide */ public void addWindowlessRoot(ViewRootImpl impl) { synchronized (mLock) { @@ -889,7 +801,7 @@ public final class WindowManagerGlobal { public void removeWindowlessRoot(ViewRootImpl impl) { synchronized (mLock) { mWindowlessRoots.remove(impl); - } + } } public void setRecentsAppBehindSystemBars(boolean behindSystemBars) { diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java index b4b1fde89a46..d7b74b3bcfe2 100644 --- a/core/java/android/view/WindowManagerImpl.java +++ b/core/java/android/view/WindowManagerImpl.java @@ -37,7 +37,6 @@ import android.os.StrictMode; import android.util.Log; import android.window.ITaskFpsCallback; import android.window.TaskFpsCallback; -import android.window.TrustedPresentationThresholds; import android.window.WindowContext; import android.window.WindowMetricsController; import android.window.WindowProvider; @@ -509,17 +508,4 @@ public final class WindowManagerImpl implements WindowManager { } return false; } - - @Override - public void registerTrustedPresentationListener(@NonNull IBinder window, - @NonNull TrustedPresentationThresholds thresholds, @NonNull Executor executor, - @NonNull Consumer listener) { - mGlobal.registerTrustedPresentationListener(window, thresholds, executor, listener); - } - - @Override - public void unregisterTrustedPresentationListener(@NonNull Consumer listener) { - mGlobal.unregisterTrustedPresentationListener(listener); - - } } diff --git a/core/java/android/window/ITrustedPresentationListener.aidl b/core/java/android/window/ITrustedPresentationListener.aidl deleted file mode 100644 index b33128abb7e5..000000000000 --- a/core/java/android/window/ITrustedPresentationListener.aidl +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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.window; - -/** - * @hide - */ -oneway interface ITrustedPresentationListener { - void onTrustedPresentationChanged(in int[] enteredTrustedStateIds, in int[] exitedTrustedStateIds); -} \ No newline at end of file diff --git a/core/java/android/window/TrustedPresentationListener.java b/core/java/android/window/TrustedPresentationListener.java deleted file mode 100644 index 02fd6d98fb0d..000000000000 --- a/core/java/android/window/TrustedPresentationListener.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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.window; - -/** - * @hide - */ -public interface TrustedPresentationListener { - - void onTrustedPresentationChanged(boolean inTrustedPresentationState); - -} diff --git a/core/java/android/window/TrustedPresentationThresholds.aidl b/core/java/android/window/TrustedPresentationThresholds.aidl deleted file mode 100644 index d7088bf0fddc..000000000000 --- a/core/java/android/window/TrustedPresentationThresholds.aidl +++ /dev/null @@ -1,3 +0,0 @@ -package android.window; - -parcelable TrustedPresentationThresholds; diff --git a/core/java/android/window/TrustedPresentationThresholds.java b/core/java/android/window/TrustedPresentationThresholds.java deleted file mode 100644 index 801d35c49228..000000000000 --- a/core/java/android/window/TrustedPresentationThresholds.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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.window; - -import android.annotation.FloatRange; -import android.annotation.IntRange; -import android.os.Parcel; -import android.os.Parcelable; -import android.view.SurfaceControl; - -import androidx.annotation.NonNull; - -/** - * @hide - */ -public final class TrustedPresentationThresholds implements Parcelable { - /** - * The min alpha the {@link SurfaceControl} is required to have to be considered inside the - * threshold. - */ - @FloatRange(from = 0f, fromInclusive = false, to = 1f) - public final float mMinAlpha; - - /** - * The min fraction of the SurfaceControl that was presented to the user to be considered - * inside the threshold. - */ - @FloatRange(from = 0f, fromInclusive = false, to = 1f) - public final float mMinFractionRendered; - - /** - * The time in milliseconds required for the {@link SurfaceControl} to be in the threshold. - */ - @IntRange(from = 1) - public final int mStabilityRequirementMs; - - private void checkValid() { - if (mMinAlpha <= 0 || mMinFractionRendered <= 0 || mStabilityRequirementMs < 1) { - throw new IllegalArgumentException( - "TrustedPresentationThresholds values are invalid"); - } - } - - /** - * Creates a new TrustedPresentationThresholds. - * - * @param minAlpha The min alpha the {@link SurfaceControl} is required to - * have to be considered inside the - * threshold. - * @param minFractionRendered The min fraction of the SurfaceControl that was presented - * to the user to be considered - * inside the threshold. - * @param stabilityRequirementMs The time in milliseconds required for the - * {@link SurfaceControl} to be in the threshold. - */ - public TrustedPresentationThresholds( - @FloatRange(from = 0f, fromInclusive = false, to = 1f) float minAlpha, - @FloatRange(from = 0f, fromInclusive = false, to = 1f) float minFractionRendered, - @IntRange(from = 1) int stabilityRequirementMs) { - this.mMinAlpha = minAlpha; - this.mMinFractionRendered = minFractionRendered; - this.mStabilityRequirementMs = stabilityRequirementMs; - checkValid(); - } - - @Override - public String toString() { - return "TrustedPresentationThresholds { " - + "minAlpha = " + mMinAlpha + ", " - + "minFractionRendered = " + mMinFractionRendered + ", " - + "stabilityRequirementMs = " + mStabilityRequirementMs - + " }"; - } - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeFloat(mMinAlpha); - dest.writeFloat(mMinFractionRendered); - dest.writeInt(mStabilityRequirementMs); - } - - @Override - public int describeContents() { - return 0; - } - - /** - * @hide - */ - TrustedPresentationThresholds(@NonNull Parcel in) { - mMinAlpha = in.readFloat(); - mMinFractionRendered = in.readFloat(); - mStabilityRequirementMs = in.readInt(); - - checkValid(); - } - - /** - * @hide - */ - public static final @NonNull Creator CREATOR = - new Creator() { - @Override - public TrustedPresentationThresholds[] newArray(int size) { - return new TrustedPresentationThresholds[size]; - } - - @Override - public TrustedPresentationThresholds createFromParcel(@NonNull Parcel in) { - return new TrustedPresentationThresholds(in); - } - }; -} diff --git a/core/java/com/android/internal/protolog/ProtoLogGroup.java b/core/java/com/android/internal/protolog/ProtoLogGroup.java index 8c2a52560050..4bb7c33b41e2 100644 --- a/core/java/com/android/internal/protolog/ProtoLogGroup.java +++ b/core/java/com/android/internal/protolog/ProtoLogGroup.java @@ -93,7 +93,6 @@ public enum ProtoLogGroup implements IProtoLogGroup { WM_DEBUG_DREAM(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, Consts.TAG_WM), WM_DEBUG_DIMMER(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM), - WM_DEBUG_TPL(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM), TEST_GROUP(true, true, false, "WindowManagerProtoLogTest"); private final boolean mEnabled; diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 19128212094d..2237ba1924db 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -595,12 +595,6 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, - "-1518132958": { - "message": "fractionRendered boundsOverSource=%f", - "level": "VERBOSE", - "group": "WM_DEBUG_TPL", - "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" - }, "-1517908912": { "message": "requestScrollCapture: caught exception dispatching to window.token=%s", "level": "WARN", @@ -967,12 +961,6 @@ "group": "WM_DEBUG_CONTENT_RECORDING", "at": "com\/android\/server\/wm\/ContentRecorder.java" }, - "-1209762265": { - "message": "Registering listener=%s with id=%d for window=%s with %s", - "level": "DEBUG", - "group": "WM_DEBUG_TPL", - "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" - }, "-1209252064": { "message": "Clear animatingExit: reason=clearAnimatingFlags win=%s", "level": "DEBUG", @@ -1345,12 +1333,6 @@ "group": "WM_DEBUG_WINDOW_TRANSITIONS", "at": "com\/android\/server\/wm\/Transition.java" }, - "-888703350": { - "message": "Skipping %s", - "level": "VERBOSE", - "group": "WM_DEBUG_TPL", - "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" - }, "-883738232": { "message": "Adding more than one toast window for UID at a time.", "level": "WARN", @@ -2821,12 +2803,6 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, - "360319850": { - "message": "fractionRendered scale=%f", - "level": "VERBOSE", - "group": "WM_DEBUG_TPL", - "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" - }, "364992694": { "message": "freezeDisplayRotation: current rotation=%d, new rotation=%d, caller=%s", "level": "VERBOSE", @@ -3007,12 +2983,6 @@ "group": "WM_DEBUG_BACK_PREVIEW", "at": "com\/android\/server\/wm\/BackNavigationController.java" }, - "532771960": { - "message": "Adding untrusted state listener=%s with id=%d", - "level": "DEBUG", - "group": "WM_DEBUG_TPL", - "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" - }, "535103992": { "message": "Wallpaper may change! Adjusting", "level": "VERBOSE", @@ -3091,12 +3061,6 @@ "group": "WM_DEBUG_DREAM", "at": "com\/android\/server\/wm\/ActivityTaskManagerService.java" }, - "605179032": { - "message": "checkIfInThreshold fractionRendered=%f alpha=%f currTimeMs=%d", - "level": "VERBOSE", - "group": "WM_DEBUG_TPL", - "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" - }, "608694300": { "message": " NEW SURFACE SESSION %s", "level": "INFO", @@ -3325,12 +3289,6 @@ "group": "WM_SHOW_TRANSACTIONS", "at": "com\/android\/server\/wm\/WindowState.java" }, - "824532141": { - "message": "lastState=%s newState=%s alpha=%f minAlpha=%f fractionRendered=%f minFractionRendered=%f", - "level": "VERBOSE", - "group": "WM_DEBUG_TPL", - "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" - }, "829434921": { "message": "Draw state now committed in %s", "level": "VERBOSE", @@ -3625,12 +3583,6 @@ "group": "WM_SHOW_SURFACE_ALLOC", "at": "com\/android\/server\/wm\/ScreenRotationAnimation.java" }, - "1090378847": { - "message": "Checking %d windows", - "level": "VERBOSE", - "group": "WM_DEBUG_TPL", - "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" - }, "1100065297": { "message": "Attempted to get IME policy of a display that does not exist: %d", "level": "WARN", @@ -3763,12 +3715,6 @@ "group": "WM_DEBUG_FOCUS", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, - "1251721200": { - "message": "unregister failed, couldn't find deathRecipient for %s with id=%d", - "level": "ERROR", - "group": "WM_DEBUG_TPL", - "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" - }, "1252594551": { "message": "Window types in WindowContext and LayoutParams.type should match! Type from LayoutParams is %d, but type from WindowContext is %d", "level": "WARN", @@ -3907,12 +3853,6 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/TaskDisplayArea.java" }, - "1382634842": { - "message": "Unregistering listener=%s with id=%d", - "level": "DEBUG", - "group": "WM_DEBUG_TPL", - "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" - }, "1393721079": { "message": "Starting remote display change: from [rot = %d], to [%dx%d, rot = %d]", "level": "VERBOSE", @@ -3961,12 +3901,6 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, - "1445704347": { - "message": "coveredRegionsAbove updated with %s frame:%s region:%s", - "level": "VERBOSE", - "group": "WM_DEBUG_TPL", - "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" - }, "1448683958": { "message": "Override pending remote transitionSet=%b adapter=%s", "level": "INFO", @@ -4267,12 +4201,6 @@ "group": "WM_DEBUG_RECENTS_ANIMATIONS", "at": "com\/android\/server\/wm\/RecentsAnimation.java" }, - "1786463281": { - "message": "Adding trusted state listener=%s with id=%d", - "level": "DEBUG", - "group": "WM_DEBUG_TPL", - "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" - }, "1789321832": { "message": "Then token:%s is invalid. It might be removed", "level": "WARN", @@ -4447,12 +4375,6 @@ "group": "WM_DEBUG_TASKS", "at": "com\/android\/server\/wm\/RootWindowContainer.java" }, - "1955470028": { - "message": "computeFractionRendered: visibleRegion=%s screenBounds=%s contentSize=%s scale=%f,%f", - "level": "VERBOSE", - "group": "WM_DEBUG_TPL", - "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" - }, "1964565370": { "message": "Starting remote animation", "level": "INFO", @@ -4737,9 +4659,6 @@ "WM_DEBUG_TASKS": { "tag": "WindowManager" }, - "WM_DEBUG_TPL": { - "tag": "WindowManager" - }, "WM_DEBUG_WALLPAPER": { "tag": "WindowManager" }, diff --git a/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java b/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java deleted file mode 100644 index e82dc37c2b6a..000000000000 --- a/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java +++ /dev/null @@ -1,448 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm; - -import static android.graphics.Matrix.MSCALE_X; -import static android.graphics.Matrix.MSCALE_Y; -import static android.graphics.Matrix.MSKEW_X; -import static android.graphics.Matrix.MSKEW_Y; - -import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TPL; - -import android.graphics.Matrix; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.Region; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; -import android.os.RemoteException; -import android.util.ArrayMap; -import android.util.ArraySet; -import android.util.IntArray; -import android.util.Pair; -import android.util.Size; -import android.view.InputWindowHandle; -import android.window.ITrustedPresentationListener; -import android.window.TrustedPresentationThresholds; -import android.window.WindowInfosListener; - -import com.android.internal.protolog.common.ProtoLog; -import com.android.server.wm.utils.RegionUtils; - -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Optional; - -/** - * Class to handle TrustedPresentationListener registrations in a thread safe manner. This class - * also takes care of cleaning up listeners when the remote process dies. - */ -public class TrustedPresentationListenerController { - - // Should only be accessed by the posting to the handler - private class Listeners { - private final class ListenerDeathRecipient implements IBinder.DeathRecipient { - IBinder mListenerBinder; - int mInstances; - - ListenerDeathRecipient(IBinder listenerBinder) { - mListenerBinder = listenerBinder; - mInstances = 0; - try { - mListenerBinder.linkToDeath(this, 0); - } catch (RemoteException ignore) { - } - } - - void addInstance() { - mInstances++; - } - - // return true if there are no instances alive - boolean removeInstance() { - mInstances--; - if (mInstances > 0) { - return false; - } - mListenerBinder.unlinkToDeath(this, 0); - return true; - } - - public void binderDied() { - mHandler.post(() -> { - mUniqueListeners.remove(mListenerBinder); - removeListeners(mListenerBinder, Optional.empty()); - }); - } - } - - // tracks binder deaths for cleanup - ArrayMap mUniqueListeners = new ArrayMap<>(); - ArrayMap> mWindowToListeners = - new ArrayMap<>(); - - void register(IBinder window, ITrustedPresentationListener listener, - TrustedPresentationThresholds thresholds, int id) { - var listenersForWindow = mWindowToListeners.computeIfAbsent(window, - iBinder -> new ArrayList<>()); - listenersForWindow.add(new TrustedPresentationInfo(thresholds, id, listener)); - - // register death listener - var listenerBinder = listener.asBinder(); - var deathRecipient = mUniqueListeners.computeIfAbsent(listenerBinder, - ListenerDeathRecipient::new); - deathRecipient.addInstance(); - } - - void unregister(ITrustedPresentationListener trustedPresentationListener, int id) { - var listenerBinder = trustedPresentationListener.asBinder(); - var deathRecipient = mUniqueListeners.get(listenerBinder); - if (deathRecipient == null) { - ProtoLog.e(WM_DEBUG_TPL, "unregister failed, couldn't find" - + " deathRecipient for %s with id=%d", trustedPresentationListener, id); - return; - } - - if (deathRecipient.removeInstance()) { - mUniqueListeners.remove(listenerBinder); - } - removeListeners(listenerBinder, Optional.of(id)); - } - - boolean isEmpty() { - return mWindowToListeners.isEmpty(); - } - - ArrayList get(IBinder windowToken) { - return mWindowToListeners.get(windowToken); - } - - private void removeListeners(IBinder listenerBinder, Optional id) { - for (int i = mWindowToListeners.size() - 1; i >= 0; i--) { - var listeners = mWindowToListeners.valueAt(i); - for (int j = listeners.size() - 1; j >= 0; j--) { - var listener = listeners.get(j); - if (listener.mListener.asBinder() == listenerBinder && (id.isEmpty() - || listener.mId == id.get())) { - listeners.remove(j); - } - } - if (listeners.isEmpty()) { - mWindowToListeners.removeAt(i); - } - } - } - } - - private final Object mHandlerThreadLock = new Object(); - private HandlerThread mHandlerThread; - private Handler mHandler; - - private WindowInfosListener mWindowInfosListener; - - Listeners mRegisteredListeners = new Listeners(); - - private InputWindowHandle[] mLastWindowHandles; - - private final Object mIgnoredWindowTokensLock = new Object(); - - private final ArraySet mIgnoredWindowTokens = new ArraySet<>(); - - private void startHandlerThreadIfNeeded() { - synchronized (mHandlerThreadLock) { - if (mHandler == null) { - mHandlerThread = new HandlerThread("WindowInfosListenerForTpl"); - mHandlerThread.start(); - mHandler = new Handler(mHandlerThread.getLooper()); - } - } - } - - void addIgnoredWindowTokens(IBinder token) { - synchronized (mIgnoredWindowTokensLock) { - mIgnoredWindowTokens.add(token); - } - } - - void removeIgnoredWindowTokens(IBinder token) { - synchronized (mIgnoredWindowTokensLock) { - mIgnoredWindowTokens.remove(token); - } - } - - void registerListener(IBinder window, ITrustedPresentationListener listener, - TrustedPresentationThresholds thresholds, int id) { - startHandlerThreadIfNeeded(); - mHandler.post(() -> { - ProtoLog.d(WM_DEBUG_TPL, "Registering listener=%s with id=%d for window=%s with %s", - listener, id, window, thresholds); - - mRegisteredListeners.register(window, listener, thresholds, id); - registerWindowInfosListener(); - // Update the initial state for the new registered listener - computeTpl(mLastWindowHandles); - }); - } - - void unregisterListener(ITrustedPresentationListener listener, int id) { - startHandlerThreadIfNeeded(); - mHandler.post(() -> { - ProtoLog.d(WM_DEBUG_TPL, "Unregistering listener=%s with id=%d", - listener, id); - - mRegisteredListeners.unregister(listener, id); - if (mRegisteredListeners.isEmpty()) { - unregisterWindowInfosListener(); - } - }); - } - - void dump(PrintWriter pw) { - final String innerPrefix = " "; - pw.println("TrustedPresentationListenerController:"); - pw.println(innerPrefix + "Active unique listeners (" - + mRegisteredListeners.mUniqueListeners.size() + "):"); - for (int i = 0; i < mRegisteredListeners.mWindowToListeners.size(); i++) { - pw.println( - innerPrefix + " window=" + mRegisteredListeners.mWindowToListeners.keyAt(i)); - final var listeners = mRegisteredListeners.mWindowToListeners.valueAt(i); - for (int j = 0; j < listeners.size(); j++) { - final var listener = listeners.get(j); - pw.println(innerPrefix + innerPrefix + " listener=" + listener.mListener.asBinder() - + " id=" + listener.mId - + " thresholds=" + listener.mThresholds); - } - } - } - - private void registerWindowInfosListener() { - if (mWindowInfosListener != null) { - return; - } - - mWindowInfosListener = new WindowInfosListener() { - @Override - public void onWindowInfosChanged(InputWindowHandle[] windowHandles, - DisplayInfo[] displayInfos) { - mHandler.post(() -> computeTpl(windowHandles)); - } - }; - mLastWindowHandles = mWindowInfosListener.register().first; - } - - private void unregisterWindowInfosListener() { - if (mWindowInfosListener == null) { - return; - } - - mWindowInfosListener.unregister(); - mWindowInfosListener = null; - mLastWindowHandles = null; - } - - private void computeTpl(InputWindowHandle[] windowHandles) { - mLastWindowHandles = windowHandles; - if (mLastWindowHandles == null || mLastWindowHandles.length == 0 - || mRegisteredListeners.isEmpty()) { - return; - } - - Rect tmpRect = new Rect(); - Matrix tmpInverseMatrix = new Matrix(); - float[] tmpMatrix = new float[9]; - Region coveredRegionsAbove = new Region(); - long currTimeMs = System.currentTimeMillis(); - ProtoLog.v(WM_DEBUG_TPL, "Checking %d windows", mLastWindowHandles.length); - - ArrayMap> listenerUpdates = - new ArrayMap<>(); - ArraySet ignoredWindowTokens; - synchronized (mIgnoredWindowTokensLock) { - ignoredWindowTokens = new ArraySet<>(mIgnoredWindowTokens); - } - for (var windowHandle : mLastWindowHandles) { - if (ignoredWindowTokens.contains(windowHandle.getWindowToken())) { - ProtoLog.v(WM_DEBUG_TPL, "Skipping %s", windowHandle.name); - continue; - } - tmpRect.set(windowHandle.frame); - var listeners = mRegisteredListeners.get(windowHandle.getWindowToken()); - if (listeners != null) { - Region region = new Region(); - region.op(tmpRect, coveredRegionsAbove, Region.Op.DIFFERENCE); - windowHandle.transform.invert(tmpInverseMatrix); - tmpInverseMatrix.getValues(tmpMatrix); - float scaleX = (float) Math.sqrt(tmpMatrix[MSCALE_X] * tmpMatrix[MSCALE_X] - + tmpMatrix[MSKEW_X] * tmpMatrix[MSKEW_X]); - float scaleY = (float) Math.sqrt(tmpMatrix[MSCALE_Y] * tmpMatrix[MSCALE_Y] - + tmpMatrix[MSKEW_Y] * tmpMatrix[MSKEW_Y]); - - float fractionRendered = computeFractionRendered(region, new RectF(tmpRect), - windowHandle.contentSize, - scaleX, scaleY); - - checkIfInThreshold(listeners, listenerUpdates, fractionRendered, windowHandle.alpha, - currTimeMs); - } - - coveredRegionsAbove.op(tmpRect, Region.Op.UNION); - ProtoLog.v(WM_DEBUG_TPL, "coveredRegionsAbove updated with %s frame:%s region:%s", - windowHandle.name, tmpRect.toShortString(), coveredRegionsAbove); - } - - for (int i = 0; i < listenerUpdates.size(); i++) { - var updates = listenerUpdates.valueAt(i); - var listener = listenerUpdates.keyAt(i); - try { - listener.onTrustedPresentationChanged(updates.first.toArray(), - updates.second.toArray()); - } catch (RemoteException ignore) { - } - } - } - - private void addListenerUpdate( - ArrayMap> listenerUpdates, - ITrustedPresentationListener listener, int id, boolean presentationState) { - var updates = listenerUpdates.get(listener); - if (updates == null) { - updates = new Pair<>(new IntArray(), new IntArray()); - listenerUpdates.put(listener, updates); - } - if (presentationState) { - updates.first.add(id); - } else { - updates.second.add(id); - } - } - - - private void checkIfInThreshold( - ArrayList listeners, - ArrayMap> listenerUpdates, - float fractionRendered, float alpha, long currTimeMs) { - ProtoLog.v(WM_DEBUG_TPL, "checkIfInThreshold fractionRendered=%f alpha=%f currTimeMs=%d", - fractionRendered, alpha, currTimeMs); - for (int i = 0; i < listeners.size(); i++) { - var trustedPresentationInfo = listeners.get(i); - var listener = trustedPresentationInfo.mListener; - boolean lastState = trustedPresentationInfo.mLastComputedTrustedPresentationState; - boolean newState = - (alpha >= trustedPresentationInfo.mThresholds.mMinAlpha) && (fractionRendered - >= trustedPresentationInfo.mThresholds.mMinFractionRendered); - trustedPresentationInfo.mLastComputedTrustedPresentationState = newState; - - ProtoLog.v(WM_DEBUG_TPL, - "lastState=%s newState=%s alpha=%f minAlpha=%f fractionRendered=%f " - + "minFractionRendered=%f", - lastState, newState, alpha, trustedPresentationInfo.mThresholds.mMinAlpha, - fractionRendered, trustedPresentationInfo.mThresholds.mMinFractionRendered); - - if (lastState && !newState) { - // We were in the trusted presentation state, but now we left it, - // emit the callback if needed - if (trustedPresentationInfo.mLastReportedTrustedPresentationState) { - trustedPresentationInfo.mLastReportedTrustedPresentationState = false; - addListenerUpdate(listenerUpdates, listener, - trustedPresentationInfo.mId, /*presentationState*/ false); - ProtoLog.d(WM_DEBUG_TPL, "Adding untrusted state listener=%s with id=%d", - listener, trustedPresentationInfo.mId); - } - // Reset the timer - trustedPresentationInfo.mEnteredTrustedPresentationStateTime = -1; - } else if (!lastState && newState) { - // We were not in the trusted presentation state, but we entered it, begin the timer - // and make sure this gets called at least once more! - trustedPresentationInfo.mEnteredTrustedPresentationStateTime = currTimeMs; - mHandler.postDelayed(() -> { - computeTpl(mLastWindowHandles); - }, (long) (trustedPresentationInfo.mThresholds.mStabilityRequirementMs * 1.5)); - } - - // Has the timer elapsed, but we are still in the state? Emit a callback if needed - if (!trustedPresentationInfo.mLastReportedTrustedPresentationState && newState && ( - currTimeMs - trustedPresentationInfo.mEnteredTrustedPresentationStateTime - > trustedPresentationInfo.mThresholds.mStabilityRequirementMs)) { - trustedPresentationInfo.mLastReportedTrustedPresentationState = true; - addListenerUpdate(listenerUpdates, listener, - trustedPresentationInfo.mId, /*presentationState*/ true); - ProtoLog.d(WM_DEBUG_TPL, "Adding trusted state listener=%s with id=%d", - listener, trustedPresentationInfo.mId); - } - } - } - - private float computeFractionRendered(Region visibleRegion, RectF screenBounds, - Size contentSize, - float sx, float sy) { - ProtoLog.v(WM_DEBUG_TPL, - "computeFractionRendered: visibleRegion=%s screenBounds=%s contentSize=%s " - + "scale=%f,%f", - visibleRegion, screenBounds, contentSize, sx, sy); - - if (contentSize.getWidth() == 0 || contentSize.getHeight() == 0) { - return -1; - } - if (screenBounds.width() == 0 || screenBounds.height() == 0) { - return -1; - } - - float fractionRendered = Math.min(sx * sy, 1.0f); - ProtoLog.v(WM_DEBUG_TPL, "fractionRendered scale=%f", fractionRendered); - - float boundsOverSourceW = screenBounds.width() / (float) contentSize.getWidth(); - float boundsOverSourceH = screenBounds.height() / (float) contentSize.getHeight(); - fractionRendered *= boundsOverSourceW * boundsOverSourceH; - ProtoLog.v(WM_DEBUG_TPL, "fractionRendered boundsOverSource=%f", fractionRendered); - // Compute the size of all the rects since they may be disconnected. - float[] visibleSize = new float[1]; - RegionUtils.forEachRect(visibleRegion, rect -> { - float size = rect.width() * rect.height(); - visibleSize[0] += size; - }); - - fractionRendered *= visibleSize[0] / (screenBounds.width() * screenBounds.height()); - return fractionRendered; - } - - private static class TrustedPresentationInfo { - boolean mLastComputedTrustedPresentationState = false; - boolean mLastReportedTrustedPresentationState = false; - long mEnteredTrustedPresentationStateTime = -1; - final TrustedPresentationThresholds mThresholds; - - final ITrustedPresentationListener mListener; - final int mId; - - private TrustedPresentationInfo(TrustedPresentationThresholds thresholds, int id, - ITrustedPresentationListener listener) { - mThresholds = thresholds; - mId = id; - mListener = listener; - checkValid(thresholds); - } - - private void checkValid(TrustedPresentationThresholds thresholds) { - if (thresholds.mMinAlpha <= 0 || thresholds.mMinFractionRendered <= 0 - || thresholds.mStabilityRequirementMs < 1) { - throw new IllegalArgumentException( - "TrustedPresentationThresholds values are invalid"); - } - } - } -} diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index eaed3f94c5f5..dd2b48bb5a3d 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -303,11 +303,9 @@ import android.window.AddToSurfaceSyncGroupResult; import android.window.ClientWindowFrames; import android.window.ISurfaceSyncGroupCompletedListener; import android.window.ITaskFpsCallback; -import android.window.ITrustedPresentationListener; import android.window.ScreenCapture; import android.window.SystemPerformanceHinter; import android.window.TaskSnapshot; -import android.window.TrustedPresentationThresholds; import android.window.WindowContainerToken; import android.window.WindowContextInfo; @@ -766,9 +764,6 @@ public class WindowManagerService extends IWindowManager.Stub private final SurfaceSyncGroupController mSurfaceSyncGroupController = new SurfaceSyncGroupController(); - final TrustedPresentationListenerController mTrustedPresentationListenerController = - new TrustedPresentationListenerController(); - @VisibleForTesting final class SettingsObserver extends ContentObserver { private final Uri mDisplayInversionEnabledUri = @@ -7176,7 +7171,6 @@ public class WindowManagerService extends IWindowManager.Stub pw.println(separator); } mSystemPerformanceHinter.dump(pw, ""); - mTrustedPresentationListenerController.dump(pw); } } @@ -9768,17 +9762,4 @@ public class WindowManagerService extends IWindowManager.Stub Binder.restoreCallingIdentity(origId); } } - - @Override - public void registerTrustedPresentationListener(IBinder window, - ITrustedPresentationListener listener, - TrustedPresentationThresholds thresholds, int id) { - mTrustedPresentationListenerController.registerListener(window, listener, thresholds, id); - } - - @Override - public void unregisterTrustedPresentationListener(ITrustedPresentationListener listener, - int id) { - mTrustedPresentationListenerController.unregisterListener(listener, id); - } } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 7bc7e2cb780b..e1f1f662c5aa 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -1189,11 +1189,6 @@ class WindowState extends WindowContainer implements WindowManagerP ProtoLog.v(WM_DEBUG_ADD_REMOVE, "Adding %s to %s", this, parentWindow); parentWindow.addChild(this, sWindowSubLayerComparator); } - - if (token.mRoundedCornerOverlay) { - mWmService.mTrustedPresentationListenerController.addIgnoredWindowTokens( - getWindowToken()); - } } @Override @@ -2398,9 +2393,6 @@ class WindowState extends WindowContainer implements WindowManagerP } mWmService.postWindowRemoveCleanupLocked(this); - - mWmService.mTrustedPresentationListenerController.removeIgnoredWindowTokens( - getWindowToken()); } @Override diff --git a/services/tests/wmtests/AndroidManifest.xml b/services/tests/wmtests/AndroidManifest.xml index a8d3fa110844..c3074bb0fee8 100644 --- a/services/tests/wmtests/AndroidManifest.xml +++ b/services/tests/wmtests/AndroidManifest.xml @@ -99,7 +99,7 @@ android:theme="@style/WhiteBackgroundTheme" android:exported="true"/> - diff --git a/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationCallbackTest.java b/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationCallbackTest.java new file mode 100644 index 000000000000..c5dd447b5b0c --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationCallbackTest.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.server.wm.ActivityManagerTestBase.createFullscreenActivityScenarioRule; +import static android.server.wm.BuildUtils.HW_TIMEOUT_MULTIPLIER; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.app.Activity; +import android.platform.test.annotations.Presubmit; +import android.server.wm.CtsWindowInfoUtils; +import android.view.SurfaceControl; +import android.view.SurfaceControl.TrustedPresentationThresholds; + +import androidx.annotation.GuardedBy; +import androidx.test.ext.junit.rules.ActivityScenarioRule; + +import com.android.server.wm.utils.CommonUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; + +import java.util.function.Consumer; + +/** + * TODO (b/287076178): Move these tests to + * {@link android.view.surfacecontrol.cts.TrustedPresentationCallbackTest} when API is made public + */ +@Presubmit +public class TrustedPresentationCallbackTest { + private static final String TAG = "TrustedPresentationCallbackTest"; + private static final int STABILITY_REQUIREMENT_MS = 500; + private static final long WAIT_TIME_MS = HW_TIMEOUT_MULTIPLIER * 2000L; + + private static final float FRACTION_VISIBLE = 0.1f; + + private final Object mResultsLock = new Object(); + @GuardedBy("mResultsLock") + private boolean mResult; + @GuardedBy("mResultsLock") + private boolean mReceivedResults; + + @Rule + public TestName mName = new TestName(); + + @Rule + public ActivityScenarioRule mActivityRule = createFullscreenActivityScenarioRule( + TestActivity.class); + + private TestActivity mActivity; + + @Before + public void setup() { + mActivityRule.getScenario().onActivity(activity -> mActivity = activity); + } + + @After + public void tearDown() { + CommonUtils.waitUntilActivityRemoved(mActivity); + } + + @Test + public void testAddTrustedPresentationListenerOnWindow() throws InterruptedException { + TrustedPresentationThresholds thresholds = new TrustedPresentationThresholds( + 1 /* minAlpha */, FRACTION_VISIBLE, STABILITY_REQUIREMENT_MS); + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + mActivity.getWindow().getRootSurfaceControl().addTrustedPresentationCallback(t, thresholds, + Runnable::run, inTrustedPresentationState -> { + synchronized (mResultsLock) { + mResult = inTrustedPresentationState; + mReceivedResults = true; + mResultsLock.notify(); + } + }); + t.apply(); + synchronized (mResultsLock) { + assertResults(); + } + } + + @Test + public void testRemoveTrustedPresentationListenerOnWindow() throws InterruptedException { + TrustedPresentationThresholds thresholds = new TrustedPresentationThresholds( + 1 /* minAlpha */, FRACTION_VISIBLE, STABILITY_REQUIREMENT_MS); + Consumer trustedPresentationCallback = inTrustedPresentationState -> { + synchronized (mResultsLock) { + mResult = inTrustedPresentationState; + mReceivedResults = true; + mResultsLock.notify(); + } + }; + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + mActivity.getWindow().getRootSurfaceControl().addTrustedPresentationCallback(t, thresholds, + Runnable::run, trustedPresentationCallback); + t.apply(); + + synchronized (mResultsLock) { + if (!mReceivedResults) { + mResultsLock.wait(WAIT_TIME_MS); + } + assertResults(); + // reset the state + mReceivedResults = false; + } + + mActivity.getWindow().getRootSurfaceControl().removeTrustedPresentationCallback(t, + trustedPresentationCallback); + t.apply(); + + synchronized (mResultsLock) { + if (!mReceivedResults) { + mResultsLock.wait(WAIT_TIME_MS); + } + // Ensure we waited the full time and never received a notify on the result from the + // callback. + assertFalse("Should never have received a callback", mReceivedResults); + // results shouldn't have changed. + assertTrue(mResult); + } + } + + @GuardedBy("mResultsLock") + private void assertResults() throws InterruptedException { + mResultsLock.wait(WAIT_TIME_MS); + + if (!mReceivedResults) { + CtsWindowInfoUtils.dumpWindowsOnScreen(TAG, "test " + mName.getMethodName()); + } + // Make sure we received the results and not just timed out + assertTrue("Timed out waiting for results", mReceivedResults); + assertTrue(mResult); + } + + public static class TestActivity extends Activity { + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationListenerTest.java b/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationListenerTest.java deleted file mode 100644 index 96b66bfd3bc0..000000000000 --- a/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationListenerTest.java +++ /dev/null @@ -1,267 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm; - -import static android.server.wm.ActivityManagerTestBase.createFullscreenActivityScenarioRule; -import static android.server.wm.BuildUtils.HW_TIMEOUT_MULTIPLIER; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.fail; - -import android.app.Activity; -import android.os.SystemClock; -import android.platform.test.annotations.Presubmit; -import android.server.wm.CtsWindowInfoUtils; -import android.util.AndroidRuntimeException; -import android.util.Log; -import android.view.SurfaceControl; -import android.view.SurfaceControlViewHost; -import android.view.View; -import android.view.WindowManager; -import android.window.TrustedPresentationThresholds; - -import androidx.annotation.GuardedBy; -import androidx.annotation.NonNull; -import androidx.test.ext.junit.rules.ActivityScenarioRule; - -import com.android.server.wm.utils.CommonUtils; - -import junit.framework.Assert; - -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TestName; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; - -/** - * TODO (b/287076178): Move these tests to - * {@link android.view.surfacecontrol.cts.TrustedPresentationListenerTest} when API is made public - */ -@Presubmit -public class TrustedPresentationListenerTest { - private static final String TAG = "TrustedPresentationListenerTest"; - private static final int STABILITY_REQUIREMENT_MS = 500; - private static final long WAIT_TIME_MS = HW_TIMEOUT_MULTIPLIER * 2000L; - - private static final float FRACTION_VISIBLE = 0.1f; - - private final List mResults = Collections.synchronizedList(new ArrayList<>()); - private CountDownLatch mReceivedResults = new CountDownLatch(1); - - private TrustedPresentationThresholds mThresholds = new TrustedPresentationThresholds( - 1 /* minAlpha */, FRACTION_VISIBLE, STABILITY_REQUIREMENT_MS); - - @Rule - public TestName mName = new TestName(); - - @Rule - public ActivityScenarioRule mActivityRule = createFullscreenActivityScenarioRule( - TestActivity.class); - - private TestActivity mActivity; - - private SurfaceControlViewHost.SurfacePackage mSurfacePackage = null; - - @Before - public void setup() { - mActivityRule.getScenario().onActivity(activity -> mActivity = activity); - mDefaultListener = new Listener(mReceivedResults); - } - - @After - public void tearDown() { - if (mSurfacePackage != null) { - new SurfaceControl.Transaction().remove(mSurfacePackage.getSurfaceControl()).apply( - true); - mSurfacePackage.release(); - } - CommonUtils.waitUntilActivityRemoved(mActivity); - - } - - private class Listener implements Consumer { - final CountDownLatch mLatch; - - Listener(CountDownLatch latch) { - mLatch = latch; - } - - @Override - public void accept(Boolean inTrustedPresentationState) { - Log.d(TAG, "onTrustedPresentationChanged " + inTrustedPresentationState); - mResults.add(inTrustedPresentationState); - mLatch.countDown(); - } - } - - private Consumer mDefaultListener; - - @Test - public void testAddTrustedPresentationListenerOnWindow() { - WindowManager windowManager = mActivity.getSystemService(WindowManager.class); - windowManager.registerTrustedPresentationListener( - mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, Runnable::run, - mDefaultListener); - assertResults(List.of(true)); - } - - @Test - public void testRemoveTrustedPresentationListenerOnWindow() throws InterruptedException { - WindowManager windowManager = mActivity.getSystemService(WindowManager.class); - windowManager.registerTrustedPresentationListener( - mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, Runnable::run, - mDefaultListener); - assertResults(List.of(true)); - // reset the latch - mReceivedResults = new CountDownLatch(1); - - windowManager.unregisterTrustedPresentationListener(mDefaultListener); - mReceivedResults.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS); - // Ensure we waited the full time and never received a notify on the result from the - // callback. - assertEquals("Should never have received a callback", mReceivedResults.getCount(), 1); - // results shouldn't have changed. - assertEquals(mResults, List.of(true)); - } - - @Test - public void testRemovingUnknownListenerIsANoop() { - WindowManager windowManager = mActivity.getSystemService(WindowManager.class); - assertNotNull(windowManager); - windowManager.unregisterTrustedPresentationListener(mDefaultListener); - } - - @Test - public void testAddDuplicateListenerThrowsException() { - WindowManager windowManager = mActivity.getSystemService(WindowManager.class); - assertNotNull(windowManager); - windowManager.registerTrustedPresentationListener( - mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, - Runnable::run, mDefaultListener); - assertThrows(AndroidRuntimeException.class, - () -> windowManager.registerTrustedPresentationListener( - mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, - Runnable::run, mDefaultListener)); - } - - @Test - public void testAddDuplicateThresholds() { - mReceivedResults = new CountDownLatch(2); - mDefaultListener = new Listener(mReceivedResults); - WindowManager windowManager = mActivity.getSystemService(WindowManager.class); - windowManager.registerTrustedPresentationListener( - mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, - Runnable::run, mDefaultListener); - - Consumer mNewListener = new Listener(mReceivedResults); - - windowManager.registerTrustedPresentationListener( - mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, - Runnable::run, mNewListener); - assertResults(List.of(true, true)); - } - - private void waitForViewAttach(View view) { - final CountDownLatch viewAttached = new CountDownLatch(1); - view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(@NonNull View v) { - viewAttached.countDown(); - } - - @Override - public void onViewDetachedFromWindow(@NonNull View v) { - - } - }); - try { - viewAttached.await(2000, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - if (!wait(viewAttached, 2000 /* waitTimeMs */)) { - fail("Couldn't attach view=" + view); - } - } - - @Test - public void testAddListenerToScvh() { - WindowManager windowManager = mActivity.getSystemService(WindowManager.class); - - var embeddedView = new View(mActivity); - mActivityRule.getScenario().onActivity(activity -> { - var attachedSurfaceControl = - mActivity.getWindow().getDecorView().getRootSurfaceControl(); - var scvh = new SurfaceControlViewHost(mActivity, mActivity.getDisplay(), - attachedSurfaceControl.getHostToken()); - mSurfacePackage = scvh.getSurfacePackage(); - scvh.setView(embeddedView, mActivity.getWindow().getDecorView().getWidth(), - mActivity.getWindow().getDecorView().getHeight()); - attachedSurfaceControl.buildReparentTransaction( - mSurfacePackage.getSurfaceControl()); - }); - - waitForViewAttach(embeddedView); - windowManager.registerTrustedPresentationListener(embeddedView.getWindowToken(), - mThresholds, - Runnable::run, mDefaultListener); - - assertResults(List.of(true)); - } - - private boolean wait(CountDownLatch latch, long waitTimeMs) { - while (true) { - long now = SystemClock.uptimeMillis(); - try { - return latch.await(waitTimeMs, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - long elapsedTime = SystemClock.uptimeMillis() - now; - waitTimeMs = Math.max(0, waitTimeMs - elapsedTime); - } - } - - } - - @GuardedBy("mResultsLock") - private void assertResults(List results) { - if (!wait(mReceivedResults, WAIT_TIME_MS)) { - try { - CtsWindowInfoUtils.dumpWindowsOnScreen(TAG, "test " + mName.getMethodName()); - } catch (InterruptedException e) { - Log.d(TAG, "Couldn't dump windows", e); - } - Assert.fail("Timed out waiting for results mReceivedResults.count=" - + mReceivedResults.getCount() + "mReceivedResults=" + mReceivedResults); - } - - // Make sure we received the results - assertEquals(results.toArray(), mResults.toArray()); - } - - public static class TestActivity extends Activity { - } -} -- cgit v1.2.3-59-g8ed1b From 8a32f02573ea0bc053a2fcf99fd1c3f90a1142c4 Mon Sep 17 00:00:00 2001 From: Matías Hernández Date: Wed, 15 Nov 2023 11:59:04 +0100 Subject: Apply DeviceEffects on rule activation/deactivation (Night mode comes later, once UiModeManager changes are ready). Test: atest ZenModeHelperTest Bug: 308673540 Change-Id: I7deba78255ebe18c088cd3456c56f7801210f876 --- core/api/test-current.txt | 4 + .../hardware/display/ColorDisplayManager.java | 4 + .../service/notification/DeviceEffectsApplier.java | 38 +++++++ .../service/notification/ZenDeviceEffects.java | 22 ++++ data/etc/privapp-permissions-platform.xml | 2 + packages/Shell/AndroidManifest.xml | 3 + .../server/display/color/ColorDisplayService.java | 4 +- .../notification/DefaultDeviceEffectsApplier.java | 71 +++++++++++++ .../notification/NotificationManagerService.java | 6 ++ .../android/server/notification/ZenModeHelper.java | 83 +++++++++++++-- .../DefaultDeviceEffectsApplierTest.java | 115 +++++++++++++++++++++ .../server/notification/ZenModeHelperTest.java | 100 ++++++++++++++++-- 12 files changed, 435 insertions(+), 17 deletions(-) create mode 100644 core/java/android/service/notification/DeviceEffectsApplier.java create mode 100644 services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java create mode 100644 services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java (limited to 'data') diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 71a05a909a09..076fddf234b9 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1597,6 +1597,10 @@ package android.hardware.display { method public void restoreDozeSettings(int); } + public final class ColorDisplayManager { + method @FlaggedApi("android.app.modes_api") @RequiresPermission(android.Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS) public boolean isSaturationActivated(); + } + public final class DisplayManager { method public boolean areUserDisabledHdrTypesAllowed(); method @RequiresPermission(android.Manifest.permission.MODIFY_USER_PREFERRED_DISPLAY_MODE) public void clearGlobalUserPreferredDisplayMode(); diff --git a/core/java/android/hardware/display/ColorDisplayManager.java b/core/java/android/hardware/display/ColorDisplayManager.java index aafa7d520632..f927b8b52912 100644 --- a/core/java/android/hardware/display/ColorDisplayManager.java +++ b/core/java/android/hardware/display/ColorDisplayManager.java @@ -17,12 +17,14 @@ package android.hardware.display; import android.Manifest; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; +import android.annotation.TestApi; import android.content.ContentResolver; import android.content.Context; import android.metrics.LogMaker; @@ -397,6 +399,8 @@ public final class ColorDisplayManager { * @return {@code true} if the display is not at full saturation * @hide */ + @TestApi + @FlaggedApi(android.app.Flags.FLAG_MODES_API) @RequiresPermission(Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS) public boolean isSaturationActivated() { return mManager.isSaturationActivated(); diff --git a/core/java/android/service/notification/DeviceEffectsApplier.java b/core/java/android/service/notification/DeviceEffectsApplier.java new file mode 100644 index 000000000000..234ff4dd0852 --- /dev/null +++ b/core/java/android/service/notification/DeviceEffectsApplier.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.service.notification; + +/** + * Responsible for making any service calls needed to apply the set of {@link ZenDeviceEffects} that + * make sense for the current platform. + * @hide + */ +public interface DeviceEffectsApplier { + /** + * Applies the {@link ZenDeviceEffects} to the device. + * + *

The supplied {@code effects} represents the "consolidated" device effects, i.e. the + * union of the effects of all the {@link ZenModeConfig.ZenRule} instances that are currently + * active. If no rules are active (or no active rules specify custom effects) then {@code + * effects} will be all-default (i.e. {@link ZenDeviceEffects#hasEffects} will return {@code + * false}. + * + *

This will be called whenever the set of consolidated effects changes (normally through + * the activation or deactivation of zen rules). + */ + void apply(ZenDeviceEffects effects); +} diff --git a/core/java/android/service/notification/ZenDeviceEffects.java b/core/java/android/service/notification/ZenDeviceEffects.java index db0b7ffa0913..0e82b6c2c7d7 100644 --- a/core/java/android/service/notification/ZenDeviceEffects.java +++ b/core/java/android/service/notification/ZenDeviceEffects.java @@ -18,6 +18,7 @@ package android.service.notification; import android.annotation.FlaggedApi; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.Flags; import android.os.Parcel; import android.os.Parcelable; @@ -359,6 +360,27 @@ public final class ZenDeviceEffects implements Parcelable { return this; } + /** + * Applies the effects that are {@code true} on the supplied {@link ZenDeviceEffects} to + * this builder (essentially logically-ORing the effect set). + * @hide + */ + @NonNull + public Builder add(@Nullable ZenDeviceEffects effects) { + if (effects == null) return this; + if (effects.shouldDisplayGrayscale()) setShouldDisplayGrayscale(true); + if (effects.shouldSuppressAmbientDisplay()) setShouldSuppressAmbientDisplay(true); + if (effects.shouldDimWallpaper()) setShouldDimWallpaper(true); + if (effects.shouldUseNightMode()) setShouldUseNightMode(true); + if (effects.shouldDisableAutoBrightness()) setShouldDisableAutoBrightness(true); + if (effects.shouldDisableTapToWake()) setShouldDisableTapToWake(true); + if (effects.shouldDisableTiltToWake()) setShouldDisableTiltToWake(true); + if (effects.shouldDisableTouch()) setShouldDisableTouch(true); + if (effects.shouldMinimizeRadioUsage()) setShouldMinimizeRadioUsage(true); + if (effects.shouldMaximizeDoze()) setShouldMaximizeDoze(true); + return this; + } + /** Builds a {@link ZenDeviceEffects} object based on the builder's state. */ @NonNull public ZenDeviceEffects build() { diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 3cf28c919f07..69a6e6d998a4 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -535,6 +535,8 @@ applications that come with the platform + + diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index b6a0c7bafa44..d12d9d665a8c 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -562,6 +562,9 @@ + + + diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java index e3aa161f001a..a313bcf1f7af 100644 --- a/services/core/java/com/android/server/display/color/ColorDisplayService.java +++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java @@ -1745,8 +1745,8 @@ public final class ColorDisplayService extends SystemService { @Override public boolean setSaturationLevel(int level) { - final boolean hasTransformsPermission = getContext() - .checkCallingPermission(Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS) + final boolean hasTransformsPermission = getContext().checkCallingOrSelfPermission( + Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS) == PackageManager.PERMISSION_GRANTED; final boolean hasLegacyPermission = getContext() .checkCallingPermission(Manifest.permission.CONTROL_DISPLAY_SATURATION) diff --git a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java new file mode 100644 index 000000000000..9fdeda4b4bd0 --- /dev/null +++ b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.notification; + +import android.app.UiModeManager; +import android.app.WallpaperManager; +import android.content.Context; +import android.hardware.display.ColorDisplayManager; +import android.os.Binder; +import android.os.PowerManager; +import android.service.notification.DeviceEffectsApplier; +import android.service.notification.ZenDeviceEffects; + +/** Default implementation for {@link DeviceEffectsApplier}. */ +class DefaultDeviceEffectsApplier implements DeviceEffectsApplier { + + private static final String SUPPRESS_AMBIENT_DISPLAY_TOKEN = + "DefaultDeviceEffectsApplier:SuppressAmbientDisplay"; + private static final int SATURATION_LEVEL_GRAYSCALE = 0; + private static final int SATURATION_LEVEL_FULL_COLOR = 100; + private static final float WALLPAPER_DIM_AMOUNT_DIMMED = 0.6f; + private static final float WALLPAPER_DIM_AMOUNT_NORMAL = 0f; + + private final ColorDisplayManager mColorDisplayManager; + private final PowerManager mPowerManager; + private final UiModeManager mUiModeManager; + private final WallpaperManager mWallpaperManager; + + DefaultDeviceEffectsApplier(Context context) { + mColorDisplayManager = context.getSystemService(ColorDisplayManager.class); + mPowerManager = context.getSystemService(PowerManager.class); + mUiModeManager = context.getSystemService(UiModeManager.class); + mWallpaperManager = context.getSystemService(WallpaperManager.class); + } + + @Override + public void apply(ZenDeviceEffects effects) { + Binder.withCleanCallingIdentity(() -> { + mPowerManager.suppressAmbientDisplay(SUPPRESS_AMBIENT_DISPLAY_TOKEN, + effects.shouldSuppressAmbientDisplay()); + + if (mColorDisplayManager != null) { + mColorDisplayManager.setSaturationLevel( + effects.shouldDisplayGrayscale() ? SATURATION_LEVEL_GRAYSCALE + : SATURATION_LEVEL_FULL_COLOR); + } + + if (mWallpaperManager != null) { + mWallpaperManager.setWallpaperDimAmount( + effects.shouldDimWallpaper() ? WALLPAPER_DIM_AMOUNT_DIMMED + : WALLPAPER_DIM_AMOUNT_NORMAL); + } + + // TODO: b/308673343 - Apply dark theme (via UiModeManager) when screen is off. + }); + } +} diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 3c6887c17e97..02845fb03119 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -2941,6 +2941,12 @@ public class NotificationManagerService extends SystemService { registerDeviceConfigChange(); migrateDefaultNAS(); maybeShowInitialReviewPermissionsNotification(); + + if (android.app.Flags.modesApi()) { + // Cannot be done earlier, as some services aren't ready until this point. + mZenModeHelper.setDeviceEffectsApplier( + new DefaultDeviceEffectsApplier(getContext())); + } } else if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) { mSnoozeHelper.scheduleRepostsForPersistedNotifications(System.currentTimeMillis()); } else if (phase == SystemService.PHASE_DEVICE_SPECIFIC_SERVICES_READY) { diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 5c37eeaba180..218519fef68b 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -27,8 +27,8 @@ import static android.service.notification.NotificationServiceProto.ROOT_CONFIG; import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE; -import android.annotation.IntDef; import android.annotation.DrawableRes; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; @@ -76,6 +76,7 @@ import android.provider.Settings; import android.provider.Settings.Global; import android.service.notification.Condition; import android.service.notification.ConditionProviderService; +import android.service.notification.DeviceEffectsApplier; import android.service.notification.ZenDeviceEffects; import android.service.notification.ZenModeConfig; import android.service.notification.ZenModeConfig.ZenRule; @@ -175,6 +176,8 @@ public class ZenModeHelper { @VisibleForTesting protected int mZenMode; @VisibleForTesting protected NotificationManager.Policy mConsolidatedPolicy; + @GuardedBy("mConfigLock") + private ZenDeviceEffects mConsolidatedDeviceEffects = new ZenDeviceEffects.Builder().build(); private int mUser = UserHandle.USER_SYSTEM; private final Object mConfigLock = new Object(); @@ -182,6 +185,8 @@ public class ZenModeHelper { @VisibleForTesting protected ZenModeConfig mConfig; @VisibleForTesting protected AudioManagerInternal mAudioManager; protected PackageManager mPm; + @GuardedBy("mConfigLock") + private DeviceEffectsApplier mDeviceEffectsApplier; private long mSuppressedEffects; public static final long SUPPRESSED_EFFECT_NOTIFICATIONS = 1; @@ -189,7 +194,7 @@ public class ZenModeHelper { public static final long SUPPRESSED_EFFECT_ALL = SUPPRESSED_EFFECT_CALLS | SUPPRESSED_EFFECT_NOTIFICATIONS; - @VisibleForTesting protected boolean mIsBootComplete; + @VisibleForTesting protected boolean mIsSystemServicesReady; private String[] mPriorityOnlyDndExemptPackages; @@ -285,10 +290,33 @@ public class ZenModeHelper { mPm = mContext.getPackageManager(); mHandler.postMetricsTimer(); cleanUpZenRules(); - mIsBootComplete = true; + mIsSystemServicesReady = true; showZenUpgradeNotification(mZenMode); } + /** + * Set the {@link DeviceEffectsApplier} used to apply the consolidated effects. + * + *

If effects were calculated previously (for example, when we loaded a {@link ZenModeConfig} + * that includes activated rules), they will be applied immediately. + */ + void setDeviceEffectsApplier(@NonNull DeviceEffectsApplier deviceEffectsApplier) { + if (!Flags.modesApi()) { + return; + } + ZenDeviceEffects consolidatedDeviceEffects; + synchronized (mConfigLock) { + if (mDeviceEffectsApplier != null) { + throw new IllegalStateException("Already set up a DeviceEffectsApplier!"); + } + mDeviceEffectsApplier = deviceEffectsApplier; + consolidatedDeviceEffects = mConsolidatedDeviceEffects; + } + if (consolidatedDeviceEffects.hasEffects()) { + applyConsolidatedDeviceEffects(); + } + } + public void onUserSwitched(int user) { loadConfigForUser(user, "onUserSwitched"); } @@ -1349,7 +1377,7 @@ public class ZenModeHelper { mConfig = config; dispatchOnConfigChanged(); - updateConsolidatedPolicy(reason); + updateAndApplyConsolidatedPolicyAndDeviceEffects(reason); } final String val = Integer.toString(config.hashCode()); Global.putString(mContext.getContentResolver(), Global.ZEN_MODE_CONFIG_ETAG, val); @@ -1398,7 +1426,7 @@ public class ZenModeHelper { ZenLog.traceSetZenMode(zen, reason); mZenMode = zen; setZenModeSetting(mZenMode); - updateConsolidatedPolicy(reason); + updateAndApplyConsolidatedPolicyAndDeviceEffects(reason); boolean shouldApplyToRinger = setRingerMode && (zen != zenBefore || ( zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS && policyHashBefore != mConsolidatedPolicy.hashCode())); @@ -1459,25 +1487,56 @@ public class ZenModeHelper { } } - private void updateConsolidatedPolicy(String reason) { + private void updateAndApplyConsolidatedPolicyAndDeviceEffects(String reason) { synchronized (mConfigLock) { if (mConfig == null) return; ZenPolicy policy = new ZenPolicy(); + ZenDeviceEffects.Builder deviceEffectsBuilder = new ZenDeviceEffects.Builder(); if (mConfig.manualRule != null) { applyCustomPolicy(policy, mConfig.manualRule); + if (Flags.modesApi()) { + deviceEffectsBuilder.add(mConfig.manualRule.zenDeviceEffects); + } } for (ZenRule automaticRule : mConfig.automaticRules.values()) { if (automaticRule.isAutomaticActive()) { applyCustomPolicy(policy, automaticRule); + if (Flags.modesApi()) { + deviceEffectsBuilder.add(automaticRule.zenDeviceEffects); + } } } + Policy newPolicy = mConfig.toNotificationPolicy(policy); if (!Objects.equals(mConsolidatedPolicy, newPolicy)) { mConsolidatedPolicy = newPolicy; dispatchOnConsolidatedPolicyChanged(); ZenLog.traceSetConsolidatedZenPolicy(mConsolidatedPolicy, reason); } + + if (Flags.modesApi()) { + ZenDeviceEffects deviceEffects = deviceEffectsBuilder.build(); + if (!deviceEffects.equals(mConsolidatedDeviceEffects)) { + mConsolidatedDeviceEffects = deviceEffects; + mHandler.postApplyDeviceEffects(); + } + } + } + } + + private void applyConsolidatedDeviceEffects() { + if (!Flags.modesApi()) { + return; + } + DeviceEffectsApplier applier; + ZenDeviceEffects effects; + synchronized (mConfigLock) { + applier = mDeviceEffectsApplier; + effects = mConsolidatedDeviceEffects; + } + if (applier != null) { + applier.apply(effects); } } @@ -1893,7 +1952,7 @@ public class ZenModeHelper { private void showZenUpgradeNotification(int zen) { final boolean isWatch = mContext.getPackageManager().hasSystemFeature( PackageManager.FEATURE_WATCH); - final boolean showNotification = mIsBootComplete + final boolean showNotification = mIsSystemServicesReady && zen != Global.ZEN_MODE_OFF && !isWatch && Settings.Secure.getInt(mContext.getContentResolver(), @@ -2067,6 +2126,7 @@ public class ZenModeHelper { private static final int MSG_DISPATCH = 1; private static final int MSG_METRICS = 2; private static final int MSG_RINGER_AUDIO = 5; + private static final int MSG_APPLY_EFFECTS = 6; private static final long METRICS_PERIOD_MS = 6 * 60 * 60 * 1000; @@ -2089,6 +2149,11 @@ public class ZenModeHelper { sendMessage(obtainMessage(MSG_RINGER_AUDIO, shouldApplyToRinger)); } + private void postApplyDeviceEffects() { + removeMessages(MSG_APPLY_EFFECTS); + sendEmptyMessage(MSG_APPLY_EFFECTS); + } + @Override public void handleMessage(Message msg) { switch (msg.what) { @@ -2101,6 +2166,10 @@ public class ZenModeHelper { case MSG_RINGER_AUDIO: boolean shouldApplyToRinger = (boolean) msg.obj; updateRingerAndAudio(shouldApplyToRinger); + break; + case MSG_APPLY_EFFECTS: + applyConsolidatedDeviceEffects(); + break; } } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java new file mode 100644 index 000000000000..5febd02a75a8 --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.notification; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +import android.app.UiModeManager; +import android.app.WallpaperManager; +import android.hardware.display.ColorDisplayManager; +import android.os.PowerManager; +import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.ZenDeviceEffects; +import android.testing.TestableContext; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class DefaultDeviceEffectsApplierTest { + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private TestableContext mContext; + private DefaultDeviceEffectsApplier mApplier; + @Mock PowerManager mPowerManager; + @Mock ColorDisplayManager mColorDisplayManager; + @Mock UiModeManager mUiModeManager; + @Mock WallpaperManager mWallpaperManager; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = new TestableContext(InstrumentationRegistry.getContext(), null); + mContext.addMockSystemService(PowerManager.class, mPowerManager); + mContext.addMockSystemService(ColorDisplayManager.class, mColorDisplayManager); + mContext.addMockSystemService(UiModeManager.class, mUiModeManager); + mContext.addMockSystemService(WallpaperManager.class, mWallpaperManager); + + mApplier = new DefaultDeviceEffectsApplier(mContext); + } + + @Test + public void apply_appliesEffects() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + + ZenDeviceEffects effects = new ZenDeviceEffects.Builder() + .setShouldSuppressAmbientDisplay(true) + .setShouldDimWallpaper(true) + .setShouldDisplayGrayscale(true) + .setShouldUseNightMode(true) + .build(); + mApplier.apply(effects); + + verify(mPowerManager).suppressAmbientDisplay(anyString(), eq(true)); + verify(mColorDisplayManager).setSaturationLevel(eq(0)); + verify(mWallpaperManager).setWallpaperDimAmount(eq(0.6f)); + verifyZeroInteractions(mUiModeManager); // Coming later; adding now so test fails then. :) + } + + @Test + public void apply_removesEffects() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + + ZenDeviceEffects noEffects = new ZenDeviceEffects.Builder().build(); + mApplier.apply(noEffects); + + verify(mPowerManager).suppressAmbientDisplay(anyString(), eq(false)); + verify(mColorDisplayManager).setSaturationLevel(eq(100)); + verify(mWallpaperManager).setWallpaperDimAmount(eq(0.0f)); + verifyZeroInteractions(mUiModeManager); + } + + @Test + public void apply_missingSomeServices_okay() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mContext.addMockSystemService(ColorDisplayManager.class, null); + mContext.addMockSystemService(WallpaperManager.class, null); + + ZenDeviceEffects effects = new ZenDeviceEffects.Builder() + .setShouldSuppressAmbientDisplay(true) + .setShouldDimWallpaper(true) + .setShouldDisplayGrayscale(true) + .setShouldUseNightMode(true) + .build(); + mApplier.apply(effects); + + verify(mPowerManager).suppressAmbientDisplay(anyString(), eq(true)); + // (And no crash from missing services). + } +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index b1fdec911d86..ef6fcedf7339 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -77,9 +77,9 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.notNull; import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; @@ -112,6 +112,7 @@ import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.provider.Settings.Global; import android.service.notification.Condition; +import android.service.notification.DeviceEffectsApplier; import android.service.notification.ZenDeviceEffects; import android.service.notification.ZenModeConfig; import android.service.notification.ZenModeConfig.ScheduleInfo; @@ -188,8 +189,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { .appendPath("test") .build(); - private static final Condition CONDITION = new Condition(CONDITION_ID, "", + private static final Condition CONDITION_TRUE = new Condition(CONDITION_ID, "", Condition.STATE_TRUE); + private static final Condition CONDITION_FALSE = new Condition(CONDITION_ID, "", + Condition.STATE_FALSE); private static final String TRIGGER_DESC = "Every Night, 10pm to 6am"; private static final int TYPE = TYPE_BEDTIME; private static final boolean ALLOW_MANUAL = true; @@ -201,6 +204,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { = NotificationManager.INTERRUPTION_FILTER_ALARMS; private static final boolean ENABLED = true; private static final int CREATION_TIME = 123; + private static final ZenDeviceEffects NO_EFFECTS = new ZenDeviceEffects.Builder().build(); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -212,6 +216,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { private TestableLooper mTestableLooper; private ZenModeHelper mZenModeHelper; private ContentResolver mContentResolver; + @Mock DeviceEffectsApplier mDeviceEffectsApplier; @Mock AppOpsManager mAppOps; TestableFlagResolver mTestFlagResolver = new TestableFlagResolver(); ZenModeEventLoggerFake mZenModeEventLogger; @@ -238,8 +243,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { when(mPackageManager.getResourcesForApplication(anyString())).thenReturn( mResources); - when(mContext.getSystemService(AppOpsManager.class)).thenReturn(mAppOps); - when(mContext.getSystemService(NotificationManager.class)).thenReturn(mNotificationManager); + mContext.addMockSystemService(AppOpsManager.class, mAppOps); + mContext.addMockSystemService(NotificationManager.class, mNotificationManager); + mConditionProviders = new ConditionProviders(mContext, new UserProfiles(), AppGlobals.getPackageManager()); mConditionProviders.addSystemProvider(new CountdownConditionProvider()); @@ -609,7 +615,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // and we're setting zen mode on Settings.Secure.putInt(mContentResolver, Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 1); Settings.Secure.putInt(mContentResolver, Settings.Secure.ZEN_SETTINGS_UPDATED, 0); - mZenModeHelper.mIsBootComplete = true; + mZenModeHelper.mIsSystemServicesReady = true; mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.setZenModeSetting(ZEN_MODE_IMPORTANT_INTERRUPTIONS); @@ -624,7 +630,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // doesn't show upgrade notification if stored settings says don't show Settings.Secure.putInt(mContentResolver, Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 0); Settings.Secure.putInt(mContentResolver, Settings.Secure.ZEN_SETTINGS_UPDATED, 0); - mZenModeHelper.mIsBootComplete = true; + mZenModeHelper.mIsSystemServicesReady = true; mZenModeHelper.setZenModeSetting(ZEN_MODE_IMPORTANT_INTERRUPTIONS); verify(mNotificationManager, never()).notify(eq(ZenModeHelper.TAG), @@ -636,7 +642,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // doesn't show upgrade notification since zen was already updated Settings.Secure.putInt(mContentResolver, Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 0); Settings.Secure.putInt(mContentResolver, Settings.Secure.ZEN_SETTINGS_UPDATED, 1); - mZenModeHelper.mIsBootComplete = true; + mZenModeHelper.mIsSystemServicesReady = true; mZenModeHelper.setZenModeSetting(ZEN_MODE_IMPORTANT_INTERRUPTIONS); verify(mNotificationManager, never()).notify(eq(ZenModeHelper.TAG), @@ -3060,7 +3066,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { rule.configurationActivity = CONFIG_ACTIVITY; rule.component = OWNER; rule.conditionId = CONDITION_ID; - rule.condition = CONDITION; + rule.condition = CONDITION_TRUE; rule.enabled = ENABLED; rule.creationTime = 123; rule.id = "id"; @@ -3349,6 +3355,84 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(false, mZenModeHelper.mConfig.automaticRules.get(createdId).snoozing); } + @Test + public void testDeviceEffects_applied() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); + + ZenDeviceEffects effects = new ZenDeviceEffects.Builder() + .setShouldSuppressAmbientDisplay(true) + .setShouldDimWallpaper(true) + .build(); + String ruleId = addRuleWithEffects(effects); + verify(mDeviceEffectsApplier, never()).apply(any()); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, CUSTOM_PKG_UID, false); + mTestableLooper.processAllMessages(); + + verify(mDeviceEffectsApplier).apply(eq(effects)); + } + + @Test + public void testDeviceEffects_onDeactivateRule_applied() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); + + ZenDeviceEffects zde = new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(); + String ruleId = addRuleWithEffects(zde); + mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, CUSTOM_PKG_UID, false); + mTestableLooper.processAllMessages(); + verify(mDeviceEffectsApplier).apply(eq(zde)); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_FALSE, CUSTOM_PKG_UID, false); + mTestableLooper.processAllMessages(); + + verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS)); + } + + @Test + public void testDeviceEffects_noChangeToConsolidatedEffects_notApplied() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); + + ZenDeviceEffects zde = new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(); + String ruleId = addRuleWithEffects(zde); + mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, CUSTOM_PKG_UID, false); + mTestableLooper.processAllMessages(); + verify(mDeviceEffectsApplier).apply(eq(zde)); + + // Now create and activate a second rule that doesn't add any more effects. + String secondRuleId = addRuleWithEffects(zde); + mZenModeHelper.setAutomaticZenRuleState(secondRuleId, CONDITION_TRUE, CUSTOM_PKG_UID, + false); + mTestableLooper.processAllMessages(); + + verifyNoMoreInteractions(mDeviceEffectsApplier); + } + + @Test + public void testDeviceEffects_activeBeforeApplierProvided_appliedWhenProvided() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + + ZenDeviceEffects zde = new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(); + String ruleId = addRuleWithEffects(zde); + verify(mDeviceEffectsApplier, never()).apply(any()); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, CUSTOM_PKG_UID, false); + mTestableLooper.processAllMessages(); + verify(mDeviceEffectsApplier, never()).apply(any()); + + mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); + verify(mDeviceEffectsApplier).apply(eq(zde)); + } + + private String addRuleWithEffects(ZenDeviceEffects effects) { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID) + .setDeviceEffects(effects) + .build(); + return mZenModeHelper.addAutomaticZenRule("pkg", rule, "", CUSTOM_PKG_UID, FROM_APP); + } + @Test public void applyGlobalZenModeAsImplicitZenRule_createsImplicitRuleAndActivatesIt() { mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); -- cgit v1.2.3-59-g8ed1b From 06ebddace6ffe6c21bb3ee11121d7bfd507e540d Mon Sep 17 00:00:00 2001 From: Vishnu Nair Date: Fri, 8 Dec 2023 04:13:36 +0000 Subject: Reland Use WindowInfosListener for TPL Move TPL logic from surfaceflinger to system server. When a TPL is registered, start listening to window info updates and start checking for whether thresholds will be crossed. This allows us to move the logic away from the composition hot path and optimize to handle multiple listeners. This change also consolidates all the TPL logic into its own thread. Bug: 290795410 Change-Id: Ib4ab68d7f116ace168bf994c0fd5c0c08c4496a6 Test: atest TrustedPresentationCallbackTest --- core/java/android/view/AttachedSurfaceControl.java | 39 -- core/java/android/view/IWindowManager.aidl | 8 + core/java/android/view/ViewRootImpl.java | 12 - core/java/android/view/WindowManager.java | 33 ++ core/java/android/view/WindowManagerGlobal.java | 94 ++++- core/java/android/view/WindowManagerImpl.java | 14 + .../window/ITrustedPresentationListener.aidl | 24 ++ .../window/TrustedPresentationListener.java | 26 ++ .../window/TrustedPresentationThresholds.aidl | 3 + .../window/TrustedPresentationThresholds.java | 127 ++++++ .../android/internal/protolog/ProtoLogGroup.java | 1 + data/etc/services.core.protolog.json | 81 ++++ .../wm/TrustedPresentationListenerController.java | 448 +++++++++++++++++++++ .../android/server/wm/WindowManagerService.java | 19 + .../java/com/android/server/wm/WindowState.java | 8 + services/tests/wmtests/AndroidManifest.xml | 2 +- .../server/wm/TrustedPresentationCallbackTest.java | 154 ------- .../server/wm/TrustedPresentationListenerTest.java | 267 ++++++++++++ 18 files changed, 1151 insertions(+), 209 deletions(-) create mode 100644 core/java/android/window/ITrustedPresentationListener.aidl create mode 100644 core/java/android/window/TrustedPresentationListener.java create mode 100644 core/java/android/window/TrustedPresentationThresholds.aidl create mode 100644 core/java/android/window/TrustedPresentationThresholds.java create mode 100644 services/core/java/com/android/server/wm/TrustedPresentationListenerController.java delete mode 100644 services/tests/wmtests/src/com/android/server/wm/TrustedPresentationCallbackTest.java create mode 100644 services/tests/wmtests/src/com/android/server/wm/TrustedPresentationListenerTest.java (limited to 'data') diff --git a/core/java/android/view/AttachedSurfaceControl.java b/core/java/android/view/AttachedSurfaceControl.java index fd5517d29d74..f28574ecb3b2 100644 --- a/core/java/android/view/AttachedSurfaceControl.java +++ b/core/java/android/view/AttachedSurfaceControl.java @@ -27,9 +27,6 @@ import android.window.SurfaceSyncGroup; import com.android.window.flags.Flags; -import java.util.concurrent.Executor; -import java.util.function.Consumer; - /** * Provides an interface to the root-Surface of a View Hierarchy or Window. This * is used in combination with the {@link android.view.SurfaceControl} API to enable @@ -196,42 +193,6 @@ public interface AttachedSurfaceControl { + "implemented before making this call."); } - /** - * Add a trusted presentation listener on the SurfaceControl associated with this window. - * - * @param t Transaction that the trusted presentation listener is added on. This should - * be applied by the caller. - * @param thresholds The {@link SurfaceControl.TrustedPresentationThresholds} that will specify - * when the to invoke the callback. - * @param executor The {@link Executor} where the callback will be invoked on. - * @param listener The {@link Consumer} that will receive the callbacks when entered or - * exited the threshold. - * - * @see SurfaceControl.Transaction#setTrustedPresentationCallback(SurfaceControl, - * SurfaceControl.TrustedPresentationThresholds, Executor, Consumer) - * - * @hide b/287076178 un-hide with API bump - */ - default void addTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t, - @NonNull SurfaceControl.TrustedPresentationThresholds thresholds, - @NonNull Executor executor, @NonNull Consumer listener) { - } - - /** - * Remove a trusted presentation listener on the SurfaceControl associated with this window. - * - * @param t Transaction that the trusted presentation listener removed on. This should - * be applied by the caller. - * @param listener The {@link Consumer} that was previously registered with - * addTrustedPresentationCallback that should be removed. - * - * @see SurfaceControl.Transaction#clearTrustedPresentationCallback(SurfaceControl) - * @hide b/287076178 un-hide with API bump - */ - default void removeTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t, - @NonNull Consumer listener) { - } - /** * Transfer the currently in progress touch gesture from the host to the requested * {@link SurfaceControlViewHost.SurfacePackage}. This requires that the diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 17bbee6d020f..36b74e39072a 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -73,6 +73,8 @@ import android.window.ISurfaceSyncGroupCompletedListener; import android.window.ITaskFpsCallback; import android.window.ScreenCapture; import android.window.WindowContextInfo; +import android.window.ITrustedPresentationListener; +import android.window.TrustedPresentationThresholds; /** * System private interface to the window manager. @@ -1075,4 +1077,10 @@ interface IWindowManager @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MONITOR_INPUT)") void unregisterDecorViewGestureListener(IDecorViewGestureListener listener, int displayId); + + void registerTrustedPresentationListener(in IBinder window, in ITrustedPresentationListener listener, + in TrustedPresentationThresholds thresholds, int id); + + + void unregisterTrustedPresentationListener(in ITrustedPresentationListener listener, int id); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index cac5387116a1..8acab2a43823 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -11913,18 +11913,6 @@ public final class ViewRootImpl implements ViewParent, scheduleTraversals(); } - @Override - public void addTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t, - @NonNull SurfaceControl.TrustedPresentationThresholds thresholds, - @NonNull Executor executor, @NonNull Consumer listener) { - t.setTrustedPresentationCallback(getSurfaceControl(), thresholds, executor, listener); - } - - @Override - public void removeTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t, - @NonNull Consumer listener) { - t.clearTrustedPresentationCallback(getSurfaceControl()); - } private void logAndTrace(String msg) { if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 046ea77f196d..f668088e6b44 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -122,7 +122,9 @@ import android.view.WindowInsets.Side.InsetsSide; import android.view.WindowInsets.Type; import android.view.WindowInsets.Type.InsetsType; import android.view.accessibility.AccessibilityNodeInfo; +import android.window.ITrustedPresentationListener; import android.window.TaskFpsCallback; +import android.window.TrustedPresentationThresholds; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -5884,4 +5886,35 @@ public interface WindowManager extends ViewManager { default boolean replaceContentOnDisplayWithSc(int displayId, @NonNull SurfaceControl sc) { throw new UnsupportedOperationException(); } + + /** + * Add a trusted presentation listener associated with a window. If the listener has already + * been registered, an AndroidRuntimeException will be thrown. + * + * @param window The Window to add the trusted presentation listener for + * @param thresholds The {@link TrustedPresentationThresholds} that will specify + * when the to invoke the callback. + * @param executor The {@link Executor} where the callback will be invoked on. + * @param listener The {@link ITrustedPresentationListener} that will receive the callbacks + * when entered or exited trusted presentation per the thresholds. + * + * @hide b/287076178 un-hide with API bump + */ + default void registerTrustedPresentationListener(@NonNull IBinder window, + @NonNull TrustedPresentationThresholds thresholds, @NonNull Executor executor, + @NonNull Consumer listener) { + throw new UnsupportedOperationException(); + } + + /** + * Removes a presentation listener associated with a window. If the listener was not previously + * registered, the call will be a noop. + * + * @hide + * @see #registerTrustedPresentationListener(IBinder, + * TrustedPresentationThresholds, Executor, Consumer) + */ + default void unregisterTrustedPresentationListener(@NonNull Consumer listener) { + throw new UnsupportedOperationException(); + } } diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java index 214f1ec3d1ec..a7d814e9ab8c 100644 --- a/core/java/android/view/WindowManagerGlobal.java +++ b/core/java/android/view/WindowManagerGlobal.java @@ -30,9 +30,13 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemProperties; import android.util.AndroidRuntimeException; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; +import android.util.Pair; import android.view.inputmethod.InputMethodManager; +import android.window.ITrustedPresentationListener; +import android.window.TrustedPresentationThresholds; import com.android.internal.util.FastPrintWriter; @@ -43,6 +47,7 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.WeakHashMap; import java.util.concurrent.Executor; +import java.util.function.Consumer; import java.util.function.IntConsumer; /** @@ -143,6 +148,9 @@ public final class WindowManagerGlobal { private Runnable mSystemPropertyUpdater; + private final TrustedPresentationListener mTrustedPresentationListener = + new TrustedPresentationListener(); + private WindowManagerGlobal() { } @@ -324,7 +332,7 @@ public final class WindowManagerGlobal { final Context context = view.getContext(); if (context != null && (context.getApplicationInfo().flags - & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) { + & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) { wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; } } @@ -482,7 +490,7 @@ public final class WindowManagerGlobal { if (who != null) { WindowLeaked leak = new WindowLeaked( what + " " + who + " has leaked window " - + root.getView() + " that was originally added here"); + + root.getView() + " that was originally added here"); leak.setStackTrace(root.getLocation().getStackTrace()); Log.e(TAG, "", leak); } @@ -790,6 +798,86 @@ public final class WindowManagerGlobal { } } + public void registerTrustedPresentationListener(@NonNull IBinder window, + @NonNull TrustedPresentationThresholds thresholds, Executor executor, + @NonNull Consumer listener) { + mTrustedPresentationListener.addListener(window, thresholds, listener, executor); + } + + public void unregisterTrustedPresentationListener(@NonNull Consumer listener) { + mTrustedPresentationListener.removeListener(listener); + } + + private final class TrustedPresentationListener extends + ITrustedPresentationListener.Stub { + private static int sId = 0; + private final ArrayMap, Pair> mListeners = + new ArrayMap<>(); + + private final Object mTplLock = new Object(); + + private void addListener(IBinder window, TrustedPresentationThresholds thresholds, + Consumer listener, Executor executor) { + synchronized (mTplLock) { + if (mListeners.containsKey(listener)) { + throw new AndroidRuntimeException("Trying to add duplicate listener"); + } + int id = sId++; + mListeners.put(listener, new Pair<>(id, executor)); + try { + WindowManagerGlobal.getWindowManagerService() + .registerTrustedPresentationListener(window, this, thresholds, id); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + } + + private void removeListener(Consumer listener) { + synchronized (mTplLock) { + var removedListener = mListeners.remove(listener); + if (removedListener == null) { + Log.i(TAG, "listener " + listener + " does not exist."); + return; + } + + try { + WindowManagerGlobal.getWindowManagerService() + .unregisterTrustedPresentationListener(this, removedListener.first); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + } + + @Override + public void onTrustedPresentationChanged(int[] inTrustedStateListenerIds, + int[] outOfTrustedStateListenerIds) { + ArrayList firedListeners = new ArrayList<>(); + synchronized (mTplLock) { + mListeners.forEach((listener, idExecutorPair) -> { + final var listenerId = idExecutorPair.first; + final var executor = idExecutorPair.second; + for (int id : inTrustedStateListenerIds) { + if (listenerId == id) { + firedListeners.add(() -> executor.execute( + () -> listener.accept(/*presentationState*/true))); + } + } + for (int id : outOfTrustedStateListenerIds) { + if (listenerId == id) { + firedListeners.add(() -> executor.execute( + () -> listener.accept(/*presentationState*/false))); + } + } + }); + } + for (int i = 0; i < firedListeners.size(); i++) { + firedListeners.get(i).run(); + } + } + } + /** @hide */ public void addWindowlessRoot(ViewRootImpl impl) { synchronized (mLock) { @@ -801,7 +889,7 @@ public final class WindowManagerGlobal { public void removeWindowlessRoot(ViewRootImpl impl) { synchronized (mLock) { mWindowlessRoots.remove(impl); - } + } } public void setRecentsAppBehindSystemBars(boolean behindSystemBars) { diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java index d7b74b3bcfe2..b4b1fde89a46 100644 --- a/core/java/android/view/WindowManagerImpl.java +++ b/core/java/android/view/WindowManagerImpl.java @@ -37,6 +37,7 @@ import android.os.StrictMode; import android.util.Log; import android.window.ITaskFpsCallback; import android.window.TaskFpsCallback; +import android.window.TrustedPresentationThresholds; import android.window.WindowContext; import android.window.WindowMetricsController; import android.window.WindowProvider; @@ -508,4 +509,17 @@ public final class WindowManagerImpl implements WindowManager { } return false; } + + @Override + public void registerTrustedPresentationListener(@NonNull IBinder window, + @NonNull TrustedPresentationThresholds thresholds, @NonNull Executor executor, + @NonNull Consumer listener) { + mGlobal.registerTrustedPresentationListener(window, thresholds, executor, listener); + } + + @Override + public void unregisterTrustedPresentationListener(@NonNull Consumer listener) { + mGlobal.unregisterTrustedPresentationListener(listener); + + } } diff --git a/core/java/android/window/ITrustedPresentationListener.aidl b/core/java/android/window/ITrustedPresentationListener.aidl new file mode 100644 index 000000000000..b33128abb7e5 --- /dev/null +++ b/core/java/android/window/ITrustedPresentationListener.aidl @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.window; + +/** + * @hide + */ +oneway interface ITrustedPresentationListener { + void onTrustedPresentationChanged(in int[] enteredTrustedStateIds, in int[] exitedTrustedStateIds); +} \ No newline at end of file diff --git a/core/java/android/window/TrustedPresentationListener.java b/core/java/android/window/TrustedPresentationListener.java new file mode 100644 index 000000000000..02fd6d98fb0d --- /dev/null +++ b/core/java/android/window/TrustedPresentationListener.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.window; + +/** + * @hide + */ +public interface TrustedPresentationListener { + + void onTrustedPresentationChanged(boolean inTrustedPresentationState); + +} diff --git a/core/java/android/window/TrustedPresentationThresholds.aidl b/core/java/android/window/TrustedPresentationThresholds.aidl new file mode 100644 index 000000000000..d7088bf0fddc --- /dev/null +++ b/core/java/android/window/TrustedPresentationThresholds.aidl @@ -0,0 +1,3 @@ +package android.window; + +parcelable TrustedPresentationThresholds; diff --git a/core/java/android/window/TrustedPresentationThresholds.java b/core/java/android/window/TrustedPresentationThresholds.java new file mode 100644 index 000000000000..801d35c49228 --- /dev/null +++ b/core/java/android/window/TrustedPresentationThresholds.java @@ -0,0 +1,127 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.window; + +import android.annotation.FloatRange; +import android.annotation.IntRange; +import android.os.Parcel; +import android.os.Parcelable; +import android.view.SurfaceControl; + +import androidx.annotation.NonNull; + +/** + * @hide + */ +public final class TrustedPresentationThresholds implements Parcelable { + /** + * The min alpha the {@link SurfaceControl} is required to have to be considered inside the + * threshold. + */ + @FloatRange(from = 0f, fromInclusive = false, to = 1f) + public final float mMinAlpha; + + /** + * The min fraction of the SurfaceControl that was presented to the user to be considered + * inside the threshold. + */ + @FloatRange(from = 0f, fromInclusive = false, to = 1f) + public final float mMinFractionRendered; + + /** + * The time in milliseconds required for the {@link SurfaceControl} to be in the threshold. + */ + @IntRange(from = 1) + public final int mStabilityRequirementMs; + + private void checkValid() { + if (mMinAlpha <= 0 || mMinFractionRendered <= 0 || mStabilityRequirementMs < 1) { + throw new IllegalArgumentException( + "TrustedPresentationThresholds values are invalid"); + } + } + + /** + * Creates a new TrustedPresentationThresholds. + * + * @param minAlpha The min alpha the {@link SurfaceControl} is required to + * have to be considered inside the + * threshold. + * @param minFractionRendered The min fraction of the SurfaceControl that was presented + * to the user to be considered + * inside the threshold. + * @param stabilityRequirementMs The time in milliseconds required for the + * {@link SurfaceControl} to be in the threshold. + */ + public TrustedPresentationThresholds( + @FloatRange(from = 0f, fromInclusive = false, to = 1f) float minAlpha, + @FloatRange(from = 0f, fromInclusive = false, to = 1f) float minFractionRendered, + @IntRange(from = 1) int stabilityRequirementMs) { + this.mMinAlpha = minAlpha; + this.mMinFractionRendered = minFractionRendered; + this.mStabilityRequirementMs = stabilityRequirementMs; + checkValid(); + } + + @Override + public String toString() { + return "TrustedPresentationThresholds { " + + "minAlpha = " + mMinAlpha + ", " + + "minFractionRendered = " + mMinFractionRendered + ", " + + "stabilityRequirementMs = " + mStabilityRequirementMs + + " }"; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeFloat(mMinAlpha); + dest.writeFloat(mMinFractionRendered); + dest.writeInt(mStabilityRequirementMs); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * @hide + */ + TrustedPresentationThresholds(@NonNull Parcel in) { + mMinAlpha = in.readFloat(); + mMinFractionRendered = in.readFloat(); + mStabilityRequirementMs = in.readInt(); + + checkValid(); + } + + /** + * @hide + */ + public static final @NonNull Creator CREATOR = + new Creator() { + @Override + public TrustedPresentationThresholds[] newArray(int size) { + return new TrustedPresentationThresholds[size]; + } + + @Override + public TrustedPresentationThresholds createFromParcel(@NonNull Parcel in) { + return new TrustedPresentationThresholds(in); + } + }; +} diff --git a/core/java/com/android/internal/protolog/ProtoLogGroup.java b/core/java/com/android/internal/protolog/ProtoLogGroup.java index 4bb7c33b41e2..8c2a52560050 100644 --- a/core/java/com/android/internal/protolog/ProtoLogGroup.java +++ b/core/java/com/android/internal/protolog/ProtoLogGroup.java @@ -93,6 +93,7 @@ public enum ProtoLogGroup implements IProtoLogGroup { WM_DEBUG_DREAM(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, Consts.TAG_WM), WM_DEBUG_DIMMER(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM), + WM_DEBUG_TPL(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM), TEST_GROUP(true, true, false, "WindowManagerProtoLogTest"); private final boolean mEnabled; diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 2237ba1924db..19128212094d 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -595,6 +595,12 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "-1518132958": { + "message": "fractionRendered boundsOverSource=%f", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "-1517908912": { "message": "requestScrollCapture: caught exception dispatching to window.token=%s", "level": "WARN", @@ -961,6 +967,12 @@ "group": "WM_DEBUG_CONTENT_RECORDING", "at": "com\/android\/server\/wm\/ContentRecorder.java" }, + "-1209762265": { + "message": "Registering listener=%s with id=%d for window=%s with %s", + "level": "DEBUG", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "-1209252064": { "message": "Clear animatingExit: reason=clearAnimatingFlags win=%s", "level": "DEBUG", @@ -1333,6 +1345,12 @@ "group": "WM_DEBUG_WINDOW_TRANSITIONS", "at": "com\/android\/server\/wm\/Transition.java" }, + "-888703350": { + "message": "Skipping %s", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "-883738232": { "message": "Adding more than one toast window for UID at a time.", "level": "WARN", @@ -2803,6 +2821,12 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "360319850": { + "message": "fractionRendered scale=%f", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "364992694": { "message": "freezeDisplayRotation: current rotation=%d, new rotation=%d, caller=%s", "level": "VERBOSE", @@ -2983,6 +3007,12 @@ "group": "WM_DEBUG_BACK_PREVIEW", "at": "com\/android\/server\/wm\/BackNavigationController.java" }, + "532771960": { + "message": "Adding untrusted state listener=%s with id=%d", + "level": "DEBUG", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "535103992": { "message": "Wallpaper may change! Adjusting", "level": "VERBOSE", @@ -3061,6 +3091,12 @@ "group": "WM_DEBUG_DREAM", "at": "com\/android\/server\/wm\/ActivityTaskManagerService.java" }, + "605179032": { + "message": "checkIfInThreshold fractionRendered=%f alpha=%f currTimeMs=%d", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "608694300": { "message": " NEW SURFACE SESSION %s", "level": "INFO", @@ -3289,6 +3325,12 @@ "group": "WM_SHOW_TRANSACTIONS", "at": "com\/android\/server\/wm\/WindowState.java" }, + "824532141": { + "message": "lastState=%s newState=%s alpha=%f minAlpha=%f fractionRendered=%f minFractionRendered=%f", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "829434921": { "message": "Draw state now committed in %s", "level": "VERBOSE", @@ -3583,6 +3625,12 @@ "group": "WM_SHOW_SURFACE_ALLOC", "at": "com\/android\/server\/wm\/ScreenRotationAnimation.java" }, + "1090378847": { + "message": "Checking %d windows", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "1100065297": { "message": "Attempted to get IME policy of a display that does not exist: %d", "level": "WARN", @@ -3715,6 +3763,12 @@ "group": "WM_DEBUG_FOCUS", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "1251721200": { + "message": "unregister failed, couldn't find deathRecipient for %s with id=%d", + "level": "ERROR", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "1252594551": { "message": "Window types in WindowContext and LayoutParams.type should match! Type from LayoutParams is %d, but type from WindowContext is %d", "level": "WARN", @@ -3853,6 +3907,12 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/TaskDisplayArea.java" }, + "1382634842": { + "message": "Unregistering listener=%s with id=%d", + "level": "DEBUG", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "1393721079": { "message": "Starting remote display change: from [rot = %d], to [%dx%d, rot = %d]", "level": "VERBOSE", @@ -3901,6 +3961,12 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "1445704347": { + "message": "coveredRegionsAbove updated with %s frame:%s region:%s", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "1448683958": { "message": "Override pending remote transitionSet=%b adapter=%s", "level": "INFO", @@ -4201,6 +4267,12 @@ "group": "WM_DEBUG_RECENTS_ANIMATIONS", "at": "com\/android\/server\/wm\/RecentsAnimation.java" }, + "1786463281": { + "message": "Adding trusted state listener=%s with id=%d", + "level": "DEBUG", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "1789321832": { "message": "Then token:%s is invalid. It might be removed", "level": "WARN", @@ -4375,6 +4447,12 @@ "group": "WM_DEBUG_TASKS", "at": "com\/android\/server\/wm\/RootWindowContainer.java" }, + "1955470028": { + "message": "computeFractionRendered: visibleRegion=%s screenBounds=%s contentSize=%s scale=%f,%f", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "1964565370": { "message": "Starting remote animation", "level": "INFO", @@ -4659,6 +4737,9 @@ "WM_DEBUG_TASKS": { "tag": "WindowManager" }, + "WM_DEBUG_TPL": { + "tag": "WindowManager" + }, "WM_DEBUG_WALLPAPER": { "tag": "WindowManager" }, diff --git a/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java b/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java new file mode 100644 index 000000000000..e82dc37c2b6a --- /dev/null +++ b/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java @@ -0,0 +1,448 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.graphics.Matrix.MSCALE_X; +import static android.graphics.Matrix.MSCALE_Y; +import static android.graphics.Matrix.MSKEW_X; +import static android.graphics.Matrix.MSKEW_Y; + +import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TPL; + +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.IntArray; +import android.util.Pair; +import android.util.Size; +import android.view.InputWindowHandle; +import android.window.ITrustedPresentationListener; +import android.window.TrustedPresentationThresholds; +import android.window.WindowInfosListener; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.server.wm.utils.RegionUtils; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Optional; + +/** + * Class to handle TrustedPresentationListener registrations in a thread safe manner. This class + * also takes care of cleaning up listeners when the remote process dies. + */ +public class TrustedPresentationListenerController { + + // Should only be accessed by the posting to the handler + private class Listeners { + private final class ListenerDeathRecipient implements IBinder.DeathRecipient { + IBinder mListenerBinder; + int mInstances; + + ListenerDeathRecipient(IBinder listenerBinder) { + mListenerBinder = listenerBinder; + mInstances = 0; + try { + mListenerBinder.linkToDeath(this, 0); + } catch (RemoteException ignore) { + } + } + + void addInstance() { + mInstances++; + } + + // return true if there are no instances alive + boolean removeInstance() { + mInstances--; + if (mInstances > 0) { + return false; + } + mListenerBinder.unlinkToDeath(this, 0); + return true; + } + + public void binderDied() { + mHandler.post(() -> { + mUniqueListeners.remove(mListenerBinder); + removeListeners(mListenerBinder, Optional.empty()); + }); + } + } + + // tracks binder deaths for cleanup + ArrayMap mUniqueListeners = new ArrayMap<>(); + ArrayMap> mWindowToListeners = + new ArrayMap<>(); + + void register(IBinder window, ITrustedPresentationListener listener, + TrustedPresentationThresholds thresholds, int id) { + var listenersForWindow = mWindowToListeners.computeIfAbsent(window, + iBinder -> new ArrayList<>()); + listenersForWindow.add(new TrustedPresentationInfo(thresholds, id, listener)); + + // register death listener + var listenerBinder = listener.asBinder(); + var deathRecipient = mUniqueListeners.computeIfAbsent(listenerBinder, + ListenerDeathRecipient::new); + deathRecipient.addInstance(); + } + + void unregister(ITrustedPresentationListener trustedPresentationListener, int id) { + var listenerBinder = trustedPresentationListener.asBinder(); + var deathRecipient = mUniqueListeners.get(listenerBinder); + if (deathRecipient == null) { + ProtoLog.e(WM_DEBUG_TPL, "unregister failed, couldn't find" + + " deathRecipient for %s with id=%d", trustedPresentationListener, id); + return; + } + + if (deathRecipient.removeInstance()) { + mUniqueListeners.remove(listenerBinder); + } + removeListeners(listenerBinder, Optional.of(id)); + } + + boolean isEmpty() { + return mWindowToListeners.isEmpty(); + } + + ArrayList get(IBinder windowToken) { + return mWindowToListeners.get(windowToken); + } + + private void removeListeners(IBinder listenerBinder, Optional id) { + for (int i = mWindowToListeners.size() - 1; i >= 0; i--) { + var listeners = mWindowToListeners.valueAt(i); + for (int j = listeners.size() - 1; j >= 0; j--) { + var listener = listeners.get(j); + if (listener.mListener.asBinder() == listenerBinder && (id.isEmpty() + || listener.mId == id.get())) { + listeners.remove(j); + } + } + if (listeners.isEmpty()) { + mWindowToListeners.removeAt(i); + } + } + } + } + + private final Object mHandlerThreadLock = new Object(); + private HandlerThread mHandlerThread; + private Handler mHandler; + + private WindowInfosListener mWindowInfosListener; + + Listeners mRegisteredListeners = new Listeners(); + + private InputWindowHandle[] mLastWindowHandles; + + private final Object mIgnoredWindowTokensLock = new Object(); + + private final ArraySet mIgnoredWindowTokens = new ArraySet<>(); + + private void startHandlerThreadIfNeeded() { + synchronized (mHandlerThreadLock) { + if (mHandler == null) { + mHandlerThread = new HandlerThread("WindowInfosListenerForTpl"); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); + } + } + } + + void addIgnoredWindowTokens(IBinder token) { + synchronized (mIgnoredWindowTokensLock) { + mIgnoredWindowTokens.add(token); + } + } + + void removeIgnoredWindowTokens(IBinder token) { + synchronized (mIgnoredWindowTokensLock) { + mIgnoredWindowTokens.remove(token); + } + } + + void registerListener(IBinder window, ITrustedPresentationListener listener, + TrustedPresentationThresholds thresholds, int id) { + startHandlerThreadIfNeeded(); + mHandler.post(() -> { + ProtoLog.d(WM_DEBUG_TPL, "Registering listener=%s with id=%d for window=%s with %s", + listener, id, window, thresholds); + + mRegisteredListeners.register(window, listener, thresholds, id); + registerWindowInfosListener(); + // Update the initial state for the new registered listener + computeTpl(mLastWindowHandles); + }); + } + + void unregisterListener(ITrustedPresentationListener listener, int id) { + startHandlerThreadIfNeeded(); + mHandler.post(() -> { + ProtoLog.d(WM_DEBUG_TPL, "Unregistering listener=%s with id=%d", + listener, id); + + mRegisteredListeners.unregister(listener, id); + if (mRegisteredListeners.isEmpty()) { + unregisterWindowInfosListener(); + } + }); + } + + void dump(PrintWriter pw) { + final String innerPrefix = " "; + pw.println("TrustedPresentationListenerController:"); + pw.println(innerPrefix + "Active unique listeners (" + + mRegisteredListeners.mUniqueListeners.size() + "):"); + for (int i = 0; i < mRegisteredListeners.mWindowToListeners.size(); i++) { + pw.println( + innerPrefix + " window=" + mRegisteredListeners.mWindowToListeners.keyAt(i)); + final var listeners = mRegisteredListeners.mWindowToListeners.valueAt(i); + for (int j = 0; j < listeners.size(); j++) { + final var listener = listeners.get(j); + pw.println(innerPrefix + innerPrefix + " listener=" + listener.mListener.asBinder() + + " id=" + listener.mId + + " thresholds=" + listener.mThresholds); + } + } + } + + private void registerWindowInfosListener() { + if (mWindowInfosListener != null) { + return; + } + + mWindowInfosListener = new WindowInfosListener() { + @Override + public void onWindowInfosChanged(InputWindowHandle[] windowHandles, + DisplayInfo[] displayInfos) { + mHandler.post(() -> computeTpl(windowHandles)); + } + }; + mLastWindowHandles = mWindowInfosListener.register().first; + } + + private void unregisterWindowInfosListener() { + if (mWindowInfosListener == null) { + return; + } + + mWindowInfosListener.unregister(); + mWindowInfosListener = null; + mLastWindowHandles = null; + } + + private void computeTpl(InputWindowHandle[] windowHandles) { + mLastWindowHandles = windowHandles; + if (mLastWindowHandles == null || mLastWindowHandles.length == 0 + || mRegisteredListeners.isEmpty()) { + return; + } + + Rect tmpRect = new Rect(); + Matrix tmpInverseMatrix = new Matrix(); + float[] tmpMatrix = new float[9]; + Region coveredRegionsAbove = new Region(); + long currTimeMs = System.currentTimeMillis(); + ProtoLog.v(WM_DEBUG_TPL, "Checking %d windows", mLastWindowHandles.length); + + ArrayMap> listenerUpdates = + new ArrayMap<>(); + ArraySet ignoredWindowTokens; + synchronized (mIgnoredWindowTokensLock) { + ignoredWindowTokens = new ArraySet<>(mIgnoredWindowTokens); + } + for (var windowHandle : mLastWindowHandles) { + if (ignoredWindowTokens.contains(windowHandle.getWindowToken())) { + ProtoLog.v(WM_DEBUG_TPL, "Skipping %s", windowHandle.name); + continue; + } + tmpRect.set(windowHandle.frame); + var listeners = mRegisteredListeners.get(windowHandle.getWindowToken()); + if (listeners != null) { + Region region = new Region(); + region.op(tmpRect, coveredRegionsAbove, Region.Op.DIFFERENCE); + windowHandle.transform.invert(tmpInverseMatrix); + tmpInverseMatrix.getValues(tmpMatrix); + float scaleX = (float) Math.sqrt(tmpMatrix[MSCALE_X] * tmpMatrix[MSCALE_X] + + tmpMatrix[MSKEW_X] * tmpMatrix[MSKEW_X]); + float scaleY = (float) Math.sqrt(tmpMatrix[MSCALE_Y] * tmpMatrix[MSCALE_Y] + + tmpMatrix[MSKEW_Y] * tmpMatrix[MSKEW_Y]); + + float fractionRendered = computeFractionRendered(region, new RectF(tmpRect), + windowHandle.contentSize, + scaleX, scaleY); + + checkIfInThreshold(listeners, listenerUpdates, fractionRendered, windowHandle.alpha, + currTimeMs); + } + + coveredRegionsAbove.op(tmpRect, Region.Op.UNION); + ProtoLog.v(WM_DEBUG_TPL, "coveredRegionsAbove updated with %s frame:%s region:%s", + windowHandle.name, tmpRect.toShortString(), coveredRegionsAbove); + } + + for (int i = 0; i < listenerUpdates.size(); i++) { + var updates = listenerUpdates.valueAt(i); + var listener = listenerUpdates.keyAt(i); + try { + listener.onTrustedPresentationChanged(updates.first.toArray(), + updates.second.toArray()); + } catch (RemoteException ignore) { + } + } + } + + private void addListenerUpdate( + ArrayMap> listenerUpdates, + ITrustedPresentationListener listener, int id, boolean presentationState) { + var updates = listenerUpdates.get(listener); + if (updates == null) { + updates = new Pair<>(new IntArray(), new IntArray()); + listenerUpdates.put(listener, updates); + } + if (presentationState) { + updates.first.add(id); + } else { + updates.second.add(id); + } + } + + + private void checkIfInThreshold( + ArrayList listeners, + ArrayMap> listenerUpdates, + float fractionRendered, float alpha, long currTimeMs) { + ProtoLog.v(WM_DEBUG_TPL, "checkIfInThreshold fractionRendered=%f alpha=%f currTimeMs=%d", + fractionRendered, alpha, currTimeMs); + for (int i = 0; i < listeners.size(); i++) { + var trustedPresentationInfo = listeners.get(i); + var listener = trustedPresentationInfo.mListener; + boolean lastState = trustedPresentationInfo.mLastComputedTrustedPresentationState; + boolean newState = + (alpha >= trustedPresentationInfo.mThresholds.mMinAlpha) && (fractionRendered + >= trustedPresentationInfo.mThresholds.mMinFractionRendered); + trustedPresentationInfo.mLastComputedTrustedPresentationState = newState; + + ProtoLog.v(WM_DEBUG_TPL, + "lastState=%s newState=%s alpha=%f minAlpha=%f fractionRendered=%f " + + "minFractionRendered=%f", + lastState, newState, alpha, trustedPresentationInfo.mThresholds.mMinAlpha, + fractionRendered, trustedPresentationInfo.mThresholds.mMinFractionRendered); + + if (lastState && !newState) { + // We were in the trusted presentation state, but now we left it, + // emit the callback if needed + if (trustedPresentationInfo.mLastReportedTrustedPresentationState) { + trustedPresentationInfo.mLastReportedTrustedPresentationState = false; + addListenerUpdate(listenerUpdates, listener, + trustedPresentationInfo.mId, /*presentationState*/ false); + ProtoLog.d(WM_DEBUG_TPL, "Adding untrusted state listener=%s with id=%d", + listener, trustedPresentationInfo.mId); + } + // Reset the timer + trustedPresentationInfo.mEnteredTrustedPresentationStateTime = -1; + } else if (!lastState && newState) { + // We were not in the trusted presentation state, but we entered it, begin the timer + // and make sure this gets called at least once more! + trustedPresentationInfo.mEnteredTrustedPresentationStateTime = currTimeMs; + mHandler.postDelayed(() -> { + computeTpl(mLastWindowHandles); + }, (long) (trustedPresentationInfo.mThresholds.mStabilityRequirementMs * 1.5)); + } + + // Has the timer elapsed, but we are still in the state? Emit a callback if needed + if (!trustedPresentationInfo.mLastReportedTrustedPresentationState && newState && ( + currTimeMs - trustedPresentationInfo.mEnteredTrustedPresentationStateTime + > trustedPresentationInfo.mThresholds.mStabilityRequirementMs)) { + trustedPresentationInfo.mLastReportedTrustedPresentationState = true; + addListenerUpdate(listenerUpdates, listener, + trustedPresentationInfo.mId, /*presentationState*/ true); + ProtoLog.d(WM_DEBUG_TPL, "Adding trusted state listener=%s with id=%d", + listener, trustedPresentationInfo.mId); + } + } + } + + private float computeFractionRendered(Region visibleRegion, RectF screenBounds, + Size contentSize, + float sx, float sy) { + ProtoLog.v(WM_DEBUG_TPL, + "computeFractionRendered: visibleRegion=%s screenBounds=%s contentSize=%s " + + "scale=%f,%f", + visibleRegion, screenBounds, contentSize, sx, sy); + + if (contentSize.getWidth() == 0 || contentSize.getHeight() == 0) { + return -1; + } + if (screenBounds.width() == 0 || screenBounds.height() == 0) { + return -1; + } + + float fractionRendered = Math.min(sx * sy, 1.0f); + ProtoLog.v(WM_DEBUG_TPL, "fractionRendered scale=%f", fractionRendered); + + float boundsOverSourceW = screenBounds.width() / (float) contentSize.getWidth(); + float boundsOverSourceH = screenBounds.height() / (float) contentSize.getHeight(); + fractionRendered *= boundsOverSourceW * boundsOverSourceH; + ProtoLog.v(WM_DEBUG_TPL, "fractionRendered boundsOverSource=%f", fractionRendered); + // Compute the size of all the rects since they may be disconnected. + float[] visibleSize = new float[1]; + RegionUtils.forEachRect(visibleRegion, rect -> { + float size = rect.width() * rect.height(); + visibleSize[0] += size; + }); + + fractionRendered *= visibleSize[0] / (screenBounds.width() * screenBounds.height()); + return fractionRendered; + } + + private static class TrustedPresentationInfo { + boolean mLastComputedTrustedPresentationState = false; + boolean mLastReportedTrustedPresentationState = false; + long mEnteredTrustedPresentationStateTime = -1; + final TrustedPresentationThresholds mThresholds; + + final ITrustedPresentationListener mListener; + final int mId; + + private TrustedPresentationInfo(TrustedPresentationThresholds thresholds, int id, + ITrustedPresentationListener listener) { + mThresholds = thresholds; + mId = id; + mListener = listener; + checkValid(thresholds); + } + + private void checkValid(TrustedPresentationThresholds thresholds) { + if (thresholds.mMinAlpha <= 0 || thresholds.mMinFractionRendered <= 0 + || thresholds.mStabilityRequirementMs < 1) { + throw new IllegalArgumentException( + "TrustedPresentationThresholds values are invalid"); + } + } + } +} diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index dd2b48bb5a3d..eaed3f94c5f5 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -303,9 +303,11 @@ import android.window.AddToSurfaceSyncGroupResult; import android.window.ClientWindowFrames; import android.window.ISurfaceSyncGroupCompletedListener; import android.window.ITaskFpsCallback; +import android.window.ITrustedPresentationListener; import android.window.ScreenCapture; import android.window.SystemPerformanceHinter; import android.window.TaskSnapshot; +import android.window.TrustedPresentationThresholds; import android.window.WindowContainerToken; import android.window.WindowContextInfo; @@ -764,6 +766,9 @@ public class WindowManagerService extends IWindowManager.Stub private final SurfaceSyncGroupController mSurfaceSyncGroupController = new SurfaceSyncGroupController(); + final TrustedPresentationListenerController mTrustedPresentationListenerController = + new TrustedPresentationListenerController(); + @VisibleForTesting final class SettingsObserver extends ContentObserver { private final Uri mDisplayInversionEnabledUri = @@ -7171,6 +7176,7 @@ public class WindowManagerService extends IWindowManager.Stub pw.println(separator); } mSystemPerformanceHinter.dump(pw, ""); + mTrustedPresentationListenerController.dump(pw); } } @@ -9762,4 +9768,17 @@ public class WindowManagerService extends IWindowManager.Stub Binder.restoreCallingIdentity(origId); } } + + @Override + public void registerTrustedPresentationListener(IBinder window, + ITrustedPresentationListener listener, + TrustedPresentationThresholds thresholds, int id) { + mTrustedPresentationListenerController.registerListener(window, listener, thresholds, id); + } + + @Override + public void unregisterTrustedPresentationListener(ITrustedPresentationListener listener, + int id) { + mTrustedPresentationListenerController.unregisterListener(listener, id); + } } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index e1f1f662c5aa..7bc7e2cb780b 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -1189,6 +1189,11 @@ class WindowState extends WindowContainer implements WindowManagerP ProtoLog.v(WM_DEBUG_ADD_REMOVE, "Adding %s to %s", this, parentWindow); parentWindow.addChild(this, sWindowSubLayerComparator); } + + if (token.mRoundedCornerOverlay) { + mWmService.mTrustedPresentationListenerController.addIgnoredWindowTokens( + getWindowToken()); + } } @Override @@ -2393,6 +2398,9 @@ class WindowState extends WindowContainer implements WindowManagerP } mWmService.postWindowRemoveCleanupLocked(this); + + mWmService.mTrustedPresentationListenerController.removeIgnoredWindowTokens( + getWindowToken()); } @Override diff --git a/services/tests/wmtests/AndroidManifest.xml b/services/tests/wmtests/AndroidManifest.xml index c3074bb0fee8..a8d3fa110844 100644 --- a/services/tests/wmtests/AndroidManifest.xml +++ b/services/tests/wmtests/AndroidManifest.xml @@ -99,7 +99,7 @@ android:theme="@style/WhiteBackgroundTheme" android:exported="true"/> - diff --git a/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationCallbackTest.java b/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationCallbackTest.java deleted file mode 100644 index c5dd447b5b0c..000000000000 --- a/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationCallbackTest.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm; - -import static android.server.wm.ActivityManagerTestBase.createFullscreenActivityScenarioRule; -import static android.server.wm.BuildUtils.HW_TIMEOUT_MULTIPLIER; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import android.app.Activity; -import android.platform.test.annotations.Presubmit; -import android.server.wm.CtsWindowInfoUtils; -import android.view.SurfaceControl; -import android.view.SurfaceControl.TrustedPresentationThresholds; - -import androidx.annotation.GuardedBy; -import androidx.test.ext.junit.rules.ActivityScenarioRule; - -import com.android.server.wm.utils.CommonUtils; - -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TestName; - -import java.util.function.Consumer; - -/** - * TODO (b/287076178): Move these tests to - * {@link android.view.surfacecontrol.cts.TrustedPresentationCallbackTest} when API is made public - */ -@Presubmit -public class TrustedPresentationCallbackTest { - private static final String TAG = "TrustedPresentationCallbackTest"; - private static final int STABILITY_REQUIREMENT_MS = 500; - private static final long WAIT_TIME_MS = HW_TIMEOUT_MULTIPLIER * 2000L; - - private static final float FRACTION_VISIBLE = 0.1f; - - private final Object mResultsLock = new Object(); - @GuardedBy("mResultsLock") - private boolean mResult; - @GuardedBy("mResultsLock") - private boolean mReceivedResults; - - @Rule - public TestName mName = new TestName(); - - @Rule - public ActivityScenarioRule mActivityRule = createFullscreenActivityScenarioRule( - TestActivity.class); - - private TestActivity mActivity; - - @Before - public void setup() { - mActivityRule.getScenario().onActivity(activity -> mActivity = activity); - } - - @After - public void tearDown() { - CommonUtils.waitUntilActivityRemoved(mActivity); - } - - @Test - public void testAddTrustedPresentationListenerOnWindow() throws InterruptedException { - TrustedPresentationThresholds thresholds = new TrustedPresentationThresholds( - 1 /* minAlpha */, FRACTION_VISIBLE, STABILITY_REQUIREMENT_MS); - SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - mActivity.getWindow().getRootSurfaceControl().addTrustedPresentationCallback(t, thresholds, - Runnable::run, inTrustedPresentationState -> { - synchronized (mResultsLock) { - mResult = inTrustedPresentationState; - mReceivedResults = true; - mResultsLock.notify(); - } - }); - t.apply(); - synchronized (mResultsLock) { - assertResults(); - } - } - - @Test - public void testRemoveTrustedPresentationListenerOnWindow() throws InterruptedException { - TrustedPresentationThresholds thresholds = new TrustedPresentationThresholds( - 1 /* minAlpha */, FRACTION_VISIBLE, STABILITY_REQUIREMENT_MS); - Consumer trustedPresentationCallback = inTrustedPresentationState -> { - synchronized (mResultsLock) { - mResult = inTrustedPresentationState; - mReceivedResults = true; - mResultsLock.notify(); - } - }; - SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - mActivity.getWindow().getRootSurfaceControl().addTrustedPresentationCallback(t, thresholds, - Runnable::run, trustedPresentationCallback); - t.apply(); - - synchronized (mResultsLock) { - if (!mReceivedResults) { - mResultsLock.wait(WAIT_TIME_MS); - } - assertResults(); - // reset the state - mReceivedResults = false; - } - - mActivity.getWindow().getRootSurfaceControl().removeTrustedPresentationCallback(t, - trustedPresentationCallback); - t.apply(); - - synchronized (mResultsLock) { - if (!mReceivedResults) { - mResultsLock.wait(WAIT_TIME_MS); - } - // Ensure we waited the full time and never received a notify on the result from the - // callback. - assertFalse("Should never have received a callback", mReceivedResults); - // results shouldn't have changed. - assertTrue(mResult); - } - } - - @GuardedBy("mResultsLock") - private void assertResults() throws InterruptedException { - mResultsLock.wait(WAIT_TIME_MS); - - if (!mReceivedResults) { - CtsWindowInfoUtils.dumpWindowsOnScreen(TAG, "test " + mName.getMethodName()); - } - // Make sure we received the results and not just timed out - assertTrue("Timed out waiting for results", mReceivedResults); - assertTrue(mResult); - } - - public static class TestActivity extends Activity { - } -} diff --git a/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationListenerTest.java b/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationListenerTest.java new file mode 100644 index 000000000000..96b66bfd3bc0 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationListenerTest.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.server.wm.ActivityManagerTestBase.createFullscreenActivityScenarioRule; +import static android.server.wm.BuildUtils.HW_TIMEOUT_MULTIPLIER; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; + +import android.app.Activity; +import android.os.SystemClock; +import android.platform.test.annotations.Presubmit; +import android.server.wm.CtsWindowInfoUtils; +import android.util.AndroidRuntimeException; +import android.util.Log; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.WindowManager; +import android.window.TrustedPresentationThresholds; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.test.ext.junit.rules.ActivityScenarioRule; + +import com.android.server.wm.utils.CommonUtils; + +import junit.framework.Assert; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * TODO (b/287076178): Move these tests to + * {@link android.view.surfacecontrol.cts.TrustedPresentationListenerTest} when API is made public + */ +@Presubmit +public class TrustedPresentationListenerTest { + private static final String TAG = "TrustedPresentationListenerTest"; + private static final int STABILITY_REQUIREMENT_MS = 500; + private static final long WAIT_TIME_MS = HW_TIMEOUT_MULTIPLIER * 2000L; + + private static final float FRACTION_VISIBLE = 0.1f; + + private final List mResults = Collections.synchronizedList(new ArrayList<>()); + private CountDownLatch mReceivedResults = new CountDownLatch(1); + + private TrustedPresentationThresholds mThresholds = new TrustedPresentationThresholds( + 1 /* minAlpha */, FRACTION_VISIBLE, STABILITY_REQUIREMENT_MS); + + @Rule + public TestName mName = new TestName(); + + @Rule + public ActivityScenarioRule mActivityRule = createFullscreenActivityScenarioRule( + TestActivity.class); + + private TestActivity mActivity; + + private SurfaceControlViewHost.SurfacePackage mSurfacePackage = null; + + @Before + public void setup() { + mActivityRule.getScenario().onActivity(activity -> mActivity = activity); + mDefaultListener = new Listener(mReceivedResults); + } + + @After + public void tearDown() { + if (mSurfacePackage != null) { + new SurfaceControl.Transaction().remove(mSurfacePackage.getSurfaceControl()).apply( + true); + mSurfacePackage.release(); + } + CommonUtils.waitUntilActivityRemoved(mActivity); + + } + + private class Listener implements Consumer { + final CountDownLatch mLatch; + + Listener(CountDownLatch latch) { + mLatch = latch; + } + + @Override + public void accept(Boolean inTrustedPresentationState) { + Log.d(TAG, "onTrustedPresentationChanged " + inTrustedPresentationState); + mResults.add(inTrustedPresentationState); + mLatch.countDown(); + } + } + + private Consumer mDefaultListener; + + @Test + public void testAddTrustedPresentationListenerOnWindow() { + WindowManager windowManager = mActivity.getSystemService(WindowManager.class); + windowManager.registerTrustedPresentationListener( + mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, Runnable::run, + mDefaultListener); + assertResults(List.of(true)); + } + + @Test + public void testRemoveTrustedPresentationListenerOnWindow() throws InterruptedException { + WindowManager windowManager = mActivity.getSystemService(WindowManager.class); + windowManager.registerTrustedPresentationListener( + mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, Runnable::run, + mDefaultListener); + assertResults(List.of(true)); + // reset the latch + mReceivedResults = new CountDownLatch(1); + + windowManager.unregisterTrustedPresentationListener(mDefaultListener); + mReceivedResults.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS); + // Ensure we waited the full time and never received a notify on the result from the + // callback. + assertEquals("Should never have received a callback", mReceivedResults.getCount(), 1); + // results shouldn't have changed. + assertEquals(mResults, List.of(true)); + } + + @Test + public void testRemovingUnknownListenerIsANoop() { + WindowManager windowManager = mActivity.getSystemService(WindowManager.class); + assertNotNull(windowManager); + windowManager.unregisterTrustedPresentationListener(mDefaultListener); + } + + @Test + public void testAddDuplicateListenerThrowsException() { + WindowManager windowManager = mActivity.getSystemService(WindowManager.class); + assertNotNull(windowManager); + windowManager.registerTrustedPresentationListener( + mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, + Runnable::run, mDefaultListener); + assertThrows(AndroidRuntimeException.class, + () -> windowManager.registerTrustedPresentationListener( + mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, + Runnable::run, mDefaultListener)); + } + + @Test + public void testAddDuplicateThresholds() { + mReceivedResults = new CountDownLatch(2); + mDefaultListener = new Listener(mReceivedResults); + WindowManager windowManager = mActivity.getSystemService(WindowManager.class); + windowManager.registerTrustedPresentationListener( + mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, + Runnable::run, mDefaultListener); + + Consumer mNewListener = new Listener(mReceivedResults); + + windowManager.registerTrustedPresentationListener( + mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, + Runnable::run, mNewListener); + assertResults(List.of(true, true)); + } + + private void waitForViewAttach(View view) { + final CountDownLatch viewAttached = new CountDownLatch(1); + view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(@NonNull View v) { + viewAttached.countDown(); + } + + @Override + public void onViewDetachedFromWindow(@NonNull View v) { + + } + }); + try { + viewAttached.await(2000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + if (!wait(viewAttached, 2000 /* waitTimeMs */)) { + fail("Couldn't attach view=" + view); + } + } + + @Test + public void testAddListenerToScvh() { + WindowManager windowManager = mActivity.getSystemService(WindowManager.class); + + var embeddedView = new View(mActivity); + mActivityRule.getScenario().onActivity(activity -> { + var attachedSurfaceControl = + mActivity.getWindow().getDecorView().getRootSurfaceControl(); + var scvh = new SurfaceControlViewHost(mActivity, mActivity.getDisplay(), + attachedSurfaceControl.getHostToken()); + mSurfacePackage = scvh.getSurfacePackage(); + scvh.setView(embeddedView, mActivity.getWindow().getDecorView().getWidth(), + mActivity.getWindow().getDecorView().getHeight()); + attachedSurfaceControl.buildReparentTransaction( + mSurfacePackage.getSurfaceControl()); + }); + + waitForViewAttach(embeddedView); + windowManager.registerTrustedPresentationListener(embeddedView.getWindowToken(), + mThresholds, + Runnable::run, mDefaultListener); + + assertResults(List.of(true)); + } + + private boolean wait(CountDownLatch latch, long waitTimeMs) { + while (true) { + long now = SystemClock.uptimeMillis(); + try { + return latch.await(waitTimeMs, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + long elapsedTime = SystemClock.uptimeMillis() - now; + waitTimeMs = Math.max(0, waitTimeMs - elapsedTime); + } + } + + } + + @GuardedBy("mResultsLock") + private void assertResults(List results) { + if (!wait(mReceivedResults, WAIT_TIME_MS)) { + try { + CtsWindowInfoUtils.dumpWindowsOnScreen(TAG, "test " + mName.getMethodName()); + } catch (InterruptedException e) { + Log.d(TAG, "Couldn't dump windows", e); + } + Assert.fail("Timed out waiting for results mReceivedResults.count=" + + mReceivedResults.getCount() + "mReceivedResults=" + mReceivedResults); + } + + // Make sure we received the results + assertEquals(results.toArray(), mResults.toArray()); + } + + public static class TestActivity extends Activity { + } +} -- cgit v1.2.3-59-g8ed1b