diff options
Diffstat (limited to 'libs')
39 files changed, 1305 insertions, 138 deletions
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 3b7eb292abc7..3ff40e0886a4 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -131,3 +131,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "enable_bubble_bar_in_persistent_task_bar" + namespace: "multitasking" + description: "Enable bubble bar to be shown in the persistent task bar" + bug: "346391377" +} diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp b/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp new file mode 100644 index 000000000000..c6dbd9b25e7f --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp @@ -0,0 +1,61 @@ +// Copyright (C) 2024 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], + default_team: "trendy_team_multitasking_windowing", +} + +android_test { + name: "WMShellMultivalentScreenshotTestsOnDevice", + srcs: [ + "src/**/*.kt", + ], + static_libs: [ + "WindowManager-Shell", + "junit", + "androidx.test.runner", + "androidx.test.rules", + "androidx.test.ext.junit", + "truth", + "platform-parametric-runner-lib", + "platform-screenshot-diff-core", + ], + libs: [ + "android.test.base", + "android.test.runner", + ], + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], + kotlincflags: ["-Xjvm-default=all"], + optimize: { + enabled: false, + }, + test_suites: ["device-tests"], + platform_apis: true, + certificate: "platform", + aaptflags: [ + "--extra-packages", + "com.android.wm.shell", + ], + manifest: "AndroidManifest.xml", + asset_dirs: ["goldens/onDevice"], +} diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifest.xml b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifest.xml new file mode 100644 index 000000000000..467dc6a5cb81 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifest.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.wm.shell.multivalentscreenshot"> + + <application android:debuggable="true" android:supportsRtl="true" > + <uses-library android:name="android.test.runner" /> + <activity + android:name="platform.test.screenshot.ScreenshotActivity" + android:exported="true"> + </activity> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:label="Multivalent screenshot tests for WindowManager-Shell" + android:targetPackage="com.android.wm.shell.multivalentscreenshot"> + </instrumentation> + + <!-- this permission is required by Tuner Service in screenshot tests --> + <uses-permission android:name="android.permission.MANAGE_USERS" /> +</manifest> diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml new file mode 100644 index 000000000000..a7a3f1313a9b --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.wm.shell.multivalentscreenshot"> + <application android:debuggable="true" android:supportsRtl="true"> + <uses-library android:name="android.test.runner" /> + <activity + android:name="platform.test.screenshot.ScreenshotActivity" + android:exported="true"> + </activity> + </application> +</manifest> diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidTest.xml b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidTest.xml new file mode 100644 index 000000000000..75793ae69d27 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidTest.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="Runs Tests for WindowManagerShellLib"> + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" /> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="install-arg" value="-t" /> + <option name="test-file-name" value="WMShellMultivalentScreenshotTestsOnDevice.apk" /> + </target_preparer> + + <option name="test-suite-tag" value="apct" /> + <option name="test-tag" value="WMShellMultivalentScreenshotTestsOnDevice" /> + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="directory-keys" value="/data/user/0/com.android.wm.shell.multivalentscreenshot/files/wmshell_screenshots" /> + <option name="collect-on-run-ended-only" value="true" /> + </metrics_collector> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.wm.shell.multivalentscreenshot" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png Binary files differnew file mode 100644 index 000000000000..eb2888199ddf --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png Binary files differnew file mode 100644 index 000000000000..eb2888199ddf --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/robolectric/config/robolectric.properties b/libs/WindowManager/Shell/multivalentScreenshotTests/robolectric/config/robolectric.properties new file mode 100644 index 000000000000..7a0527ccaafb --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/robolectric/config/robolectric.properties @@ -0,0 +1,2 @@ +sdk=NEWEST_SDK + diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt new file mode 100644 index 000000000000..d35f493a8f60 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.bubbles + +import android.view.LayoutInflater +import com.android.wm.shell.common.bubbles.BubblePopupView +import com.android.wm.shell.testing.goldenpathmanager.WMShellGoldenPathManager +import com.android.wm.shell.R +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters +import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.Displays +import platform.test.screenshot.ViewScreenshotTestRule +import platform.test.screenshot.getEmulatedDevicePathConfig + +@RunWith(ParameterizedAndroidJunit4::class) +class BubbleEducationViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun getTestSpecs() = DeviceEmulationSpec.forDisplays(Displays.Phone, isLandscape = false) + } + + @get:Rule + val screenshotRule = + ViewScreenshotTestRule( + emulationSpec, + WMShellGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)) + ) + + @Test + fun bubblesEducation() { + screenshotRule.screenshotTest("bubbles_education") { activity -> + activity.actionBar?.hide() + val view = + LayoutInflater.from(activity) + .inflate(R.layout.bubble_bar_stack_education, null) as BubblePopupView + view.setup() + view + } + } +} diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/testing/goldenpathmanager/WMShellGoldenPathManager.kt b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/testing/goldenpathmanager/WMShellGoldenPathManager.kt new file mode 100644 index 000000000000..901b79b9b1a0 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/testing/goldenpathmanager/WMShellGoldenPathManager.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.testing.goldenpathmanager + +import android.os.Build +import androidx.test.platform.app.InstrumentationRegistry +import platform.test.screenshot.GoldenPathManager +import platform.test.screenshot.PathConfig + +/** A WM Shell specific implementation of [GoldenPathManager]. */ +class WMShellGoldenPathManager(pathConfig: PathConfig) : + GoldenPathManager( + appContext = InstrumentationRegistry.getInstrumentation().context, + assetsPathRelativeToBuildRoot = assetPath, + deviceLocalPath = deviceLocalPath, + pathConfig = pathConfig, + ) { + + private companion object { + private const val ASSETS_PATH = + "frameworks/base/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice" + private const val ASSETS_PATH_ROBO = + "frameworks/base/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/" + + "robolectric" + private val assetPath: String + get() = if (Build.FINGERPRINT.contains("robolectric")) ASSETS_PATH_ROBO else ASSETS_PATH + private val deviceLocalPath: String + get() = + InstrumentationRegistry.getInstrumentation() + .targetContext + .filesDir + .absolutePath + .toString() + "/wmshell_screenshots" + } + override fun toString(): String { + // This string is appended to all actual/expected screenshots on the device, so make sure + // it is a static value. + return "WMShellGoldenPathManager" + } +} diff --git a/libs/WindowManager/Shell/multivalentScreenshotTestsForDevice b/libs/WindowManager/Shell/multivalentScreenshotTestsForDevice new file mode 120000 index 000000000000..e879efc81ec1 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTestsForDevice @@ -0,0 +1 @@ +multivalentScreenshotTests
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml index e537f0a80144..d7e23fd8dfd8 100644 --- a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml @@ -84,10 +84,8 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> - <!-- no translation found for bubble_shortcut_label (666269077944378311) --> - <skip /> - <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> - <skip /> + <string name="bubble_shortcut_label" msgid="666269077944378311">"Bubbles"</string> + <string name="bubble_shortcut_long_label" msgid="6088437544312894043">"Show Bubbles"</string> <string name="restart_button_description" msgid="4564728020654658478">"Tap to restart this app for a better view"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Change this app\'s aspect ratio in Settings"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Change aspect ratio"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml index bdcd275d9c14..1da8c275ce54 100644 --- a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml @@ -84,10 +84,8 @@ <string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> - <!-- no translation found for bubble_shortcut_label (666269077944378311) --> - <skip /> - <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> - <skip /> + <string name="bubble_shortcut_label" msgid="666269077944378311">"Bubbles"</string> + <string name="bubble_shortcut_long_label" msgid="6088437544312894043">"Show Bubbles"</string> <string name="restart_button_description" msgid="4564728020654658478">"Tap to restart this app for a better view"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Change this app\'s aspect ratio in Settings"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Change aspect ratio"</string> diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml index 39100425a9ac..4b9be47f8023 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings.xml @@ -53,7 +53,7 @@ <string name="accessibility_split_top" msgid="2789329702027147146">"تقسیم از بالا"</string> <string name="accessibility_split_bottom" msgid="8694551025220868191">"تقسیم از پایین"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"استفاده از حالت یکدستی"</string> - <string name="one_handed_tutorial_description" msgid="3486582858591353067">"برای خارج شدن، از پایین صفحهنمایش تند بهطرف بالا بکشید یا در هر جایی از بالای برنامه که میخواهید ضربه بزنید"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"برای خارج شدن، از پایین صفحهنمایش تند بهطرف بالا بکشید یا در هر جایی از بالای برنامه که میخواهید تکضرب بزنید"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"آغاز «حالت یکدستی»"</string> <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"خروج از «حالت یکدستی»"</string> <string name="bubbles_settings_button_description" msgid="1301286017420516912">"تنظیمات برای حبابکهای <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> @@ -71,16 +71,16 @@ <string name="bubble_dismiss_text" msgid="8816558050659478158">"رد کردن حبابک"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"مکالمه در حباب نشان داده نشود"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"گپ بااستفاده از حبابکها"</string> - <string name="bubbles_user_education_description" msgid="4215862563054175407">"مکالمههای جدید بهصورت نمادهای شناور یا حبابکها نشان داده میشوند. برای باز کردن حبابکها ضربه بزنید. برای جابهجایی، آن را بکشید."</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"مکالمههای جدید بهصورت نمادهای شناور یا حبابکها نشان داده میشوند. برای باز کردن حبابکها تکضرب بزنید. برای جابهجایی، آن را بکشید."</string> <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"کنترل حبابکها در هرزمانی"</string> - <string name="bubbles_user_education_manage" msgid="3460756219946517198">"برای خاموش کردن حبابکها از این برنامه، روی «مدیریت» ضربه بزنید"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"برای خاموش کردن حبابکها از این برنامه، روی «مدیریت» تکضرب بزنید"</string> <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"متوجهام"</string> <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"هیچ حبابک جدیدی وجود ندارد"</string> <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"حبابکهای اخیر و حبابکهای ردشده اینجا ظاهر خواهند شد"</string> <string name="bubble_bar_education_stack_title" msgid="2486903590422497245">"گپ زدن بااستفاده از حبابک"</string> - <string name="bubble_bar_education_stack_text" msgid="2446934610817409820">"مکالمههای جدید بهصورت نماد در گوشه پایین صفحهنمایش نشان داده میشود. برای ازهم بازکردن آنها ضربه بزنید یا برای بستن، آنها را بکشید."</string> + <string name="bubble_bar_education_stack_text" msgid="2446934610817409820">"مکالمههای جدید بهصورت نماد در گوشه پایین صفحهنمایش نشان داده میشود. برای ازهم بازکردن آنها تکضرب بزنید یا برای بستن، آنها را بکشید."</string> <string name="bubble_bar_education_manage_title" msgid="6148404487810835924">"کنترل حبابکها در هرزمانی"</string> - <string name="bubble_bar_education_manage_text" msgid="3199732148641842038">"برای مدیریت اینکه کدام برنامهها و مکالمهها حباب داشته باشند، ضربه بزنید"</string> + <string name="bubble_bar_education_manage_text" msgid="3199732148641842038">"برای مدیریت اینکه کدام برنامهها و مکالمهها حباب داشته باشند، تکضرب بزنید"</string> <string name="notification_bubble_title" msgid="6082910224488253378">"حباب"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"مدیریت"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"حبابک رد شد."</string> @@ -88,12 +88,12 @@ <skip /> <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) --> <skip /> - <string name="restart_button_description" msgid="4564728020654658478">"برای داشتن نمایی بهتر، ضربه بزنید تا این برنامه بازراهاندازی شود"</string> + <string name="restart_button_description" msgid="4564728020654658478">"برای داشتن نمایی بهتر، تکضرب بزنید تا این برنامه بازراهاندازی شود"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"نسبت ابعادی این برنامه را در «تنظیمات» تغییر دهید"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"تغییر نسبت ابعادی"</string> - <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"دوربین مشکل دارد؟\nبرای تنظیم مجدد اندازه ضربه بزنید"</string> - <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"مشکل برطرف نشد؟\nبرای برگرداندن ضربه بزنید"</string> - <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"دوربین مشکلی ندارد؟ برای بستن ضربه بزنید."</string> + <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"دوربین مشکل دارد؟\nبرای تنظیم مجدد اندازه تکضرب بزنید"</string> + <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"مشکل برطرف نشد؟\nبرای برگرداندن تکضرب بزنید"</string> + <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"دوربین مشکلی ندارد؟ برای بستن تکضرب بزنید."</string> <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"از چندین برنامه بهطور همزمان استفاده کنید"</string> <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"برای حالت صفحهٔ دونیمه، در برنامهای دیگر بکشید"</string> <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"برای جابهجا کردن برنامه، بیرون از آن دوضربه بزنید"</string> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index bfac24b81d2f..2520c25613e7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -258,9 +258,15 @@ public class CompatUIController implements OnDisplaysChangedListener, return; } // We're showing the first reachability education so we ignore incoming TaskInfo - // until the education flow has completed or we double tap. + // until the education flow has completed or we double tap. The double-tap + // basically cancel all the onboarding flow. We don't have to ignore events in case + // the app is in size compat mode. if (mIsFirstReachabilityEducationRunning) { - return; + if (!taskInfo.appCompatTaskInfo.isFromLetterboxDoubleTap + && !taskInfo.appCompatTaskInfo.topActivityInSizeCompat) { + return; + } + mIsFirstReachabilityEducationRunning = false; } if (taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed) { if (taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled) { @@ -278,17 +284,24 @@ public class CompatUIController implements OnDisplaysChangedListener, final boolean isFirstTimeVerticalReachabilityEdu = !topActivityPillarboxed && !mCompatUIConfiguration.hasSeenVerticalReachabilityEducation(taskInfo); if (isFirstTimeHorizontalReachabilityEdu || isFirstTimeVerticalReachabilityEdu) { - mIsFirstReachabilityEducationRunning = true; mCompatUIConfiguration.setSeenLetterboxEducation(taskInfo.userId); - createOrUpdateReachabilityEduLayout(taskInfo, taskListener); - return; + // We activate the first reachability education if the double-tap is enabled. + // If the double tap is not enabled (e.g. thin letterbox) we just set the value + // of the education being seen. + if (taskInfo.appCompatTaskInfo.isLetterboxDoubleTapEnabled) { + mIsFirstReachabilityEducationRunning = true; + createOrUpdateReachabilityEduLayout(taskInfo, taskListener); + return; + } } } } createOrUpdateCompatLayout(taskInfo, taskListener); createOrUpdateRestartDialogLayout(taskInfo, taskListener); if (mCompatUIConfiguration.getHasSeenLetterboxEducation(taskInfo.userId)) { - createOrUpdateReachabilityEduLayout(taskInfo, taskListener); + if (taskInfo.appCompatTaskInfo.isLetterboxDoubleTapEnabled) { + createOrUpdateReachabilityEduLayout(taskInfo, taskListener); + } // The user aspect ratio button should not be handled when a new TaskInfo is // sent because of a double tap or when in multi-window mode. if (taskInfo.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 1fcfa7fcf350..43cdcca19c3d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -17,6 +17,7 @@ package com.android.wm.shell.dagger; import android.annotation.Nullable; +import android.app.KeyguardManager; import android.content.Context; import android.content.pm.LauncherApps; import android.os.Handler; @@ -514,6 +515,7 @@ public abstract class WMShellModule { RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, DragAndDropController dragAndDropController, Transitions transitions, + KeyguardManager keyguardManager, EnterDesktopTaskTransitionHandler enterDesktopTransitionHandler, ExitDesktopTaskTransitionHandler exitDesktopTransitionHandler, ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler, @@ -528,7 +530,7 @@ public abstract class WMShellModule { Optional<RecentTasksController> recentTasksController) { return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController, displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, - dragAndDropController, transitions, enterDesktopTransitionHandler, + dragAndDropController, transitions, keyguardManager, enterDesktopTransitionHandler, exitDesktopTransitionHandler, toggleResizeDesktopTaskTransitionHandler, dragToDesktopTransitionHandler, desktopModeTaskRepository, desktopModeLoggerTransitionObserver, launchAdjacentController, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 196538248709..5813f8513b06 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager.RunningTaskInfo import android.app.ActivityOptions +import android.app.KeyguardManager import android.app.PendingIntent import android.app.TaskInfo import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME @@ -108,6 +109,7 @@ class DesktopTasksController( private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, private val dragAndDropController: DragAndDropController, private val transitions: Transitions, + private val keyguardManager: KeyguardManager, private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler, private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler, private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler, @@ -972,6 +974,12 @@ class DesktopTasksController( transition: IBinder ): WindowContainerTransaction? { KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFreeformTaskLaunch") + if (keyguardManager.isKeyguardLocked) { + // Do NOT handle freeform task launch when locked. + // It will be launched in fullscreen windowing mode (Details: b/160925539) + KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: skip keyguard is locked") + return null + } if (!desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java index 9f3c519b441b..ad298dcc253e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java @@ -65,9 +65,11 @@ import com.android.wm.shell.util.SplitBounds; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -394,6 +396,7 @@ public class RecentTasksController implements TaskStackListenerCallback, } ArrayList<ActivityManager.RecentTaskInfo> freeformTasks = new ArrayList<>(); + Set<Integer> minimizedFreeformTasks = new HashSet<>(); int mostRecentFreeformTaskIndex = Integer.MAX_VALUE; @@ -414,6 +417,9 @@ public class RecentTasksController implements TaskStackListenerCallback, mostRecentFreeformTaskIndex = recentTasks.size(); } freeformTasks.add(taskInfo); + if (mDesktopModeTaskRepository.get().isMinimizedTask(taskInfo.taskId)) { + minimizedFreeformTasks.add(taskInfo.taskId); + } continue; } @@ -431,8 +437,10 @@ public class RecentTasksController implements TaskStackListenerCallback, // Add a special entry for freeform tasks if (!freeformTasks.isEmpty()) { - recentTasks.add(mostRecentFreeformTaskIndex, GroupedRecentTaskInfo.forFreeformTasks( - freeformTasks.toArray(new ActivityManager.RecentTaskInfo[0]))); + recentTasks.add(mostRecentFreeformTaskIndex, + GroupedRecentTaskInfo.forFreeformTasks( + freeformTasks.toArray(new ActivityManager.RecentTaskInfo[0]), + minimizedFreeformTasks)); } return recentTasks; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt index 7c5f10a5bcca..8ee72b499e5a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt @@ -76,21 +76,40 @@ class TaskStackTransitionObserver( continue } + // Filter out changes that we care about if (change.mode == WindowManager.TRANSIT_OPEN) { change.taskInfo?.let { taskInfoList.add(it) } transitionTypeList.add(change.mode) } } - transitionToTransitionChanges.put( - transition, - TransitionChanges(taskInfoList, transitionTypeList) - ) + // Only add the transition to map if it has a change we care about + if (taskInfoList.isNotEmpty()) { + transitionToTransitionChanges.put( + transition, + TransitionChanges(taskInfoList, transitionTypeList) + ) + } } } override fun onTransitionStarting(transition: IBinder) {} - override fun onTransitionMerged(merged: IBinder, playing: IBinder) {} + override fun onTransitionMerged(merged: IBinder, playing: IBinder) { + val mergedTransitionChanges = + transitionToTransitionChanges.get(merged) + ?: + // We are adding changes of the merged transition to changes of the playing + // transition so if there is no changes nothing to do. + return + + transitionToTransitionChanges.remove(merged) + val playingTransitionChanges = transitionToTransitionChanges.get(playing) + if (playingTransitionChanges != null) { + playingTransitionChanges.merge(mergedTransitionChanges) + } else { + transitionToTransitionChanges.put(playing, mergedTransitionChanges) + } + } override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { val taskInfoList = @@ -138,6 +157,11 @@ class TaskStackTransitionObserver( private data class TransitionChanges( val taskInfoList: MutableList<RunningTaskInfo> = ArrayList(), - val transitionTypeList: MutableList<Int> = ArrayList() - ) + val transitionTypeList: MutableList<Int> = ArrayList(), + ) { + fun merge(transitionChanges: TransitionChanges) { + taskInfoList.addAll(transitionChanges.taskInfoList) + transitionTypeList.addAll(transitionChanges.transitionTypeList) + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 9412b2b0b243..9db153f2a5c5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -53,6 +53,7 @@ import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CHANGE; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_CLOSE; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_OPEN; @@ -944,12 +945,15 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } private static int getWallpaperTransitType(TransitionInfo info) { + boolean hasWallpaper = false; boolean hasOpenWallpaper = false; boolean hasCloseWallpaper = false; for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); - if ((change.getFlags() & FLAG_SHOW_WALLPAPER) != 0) { + if ((change.getFlags() & FLAG_SHOW_WALLPAPER) != 0 + || (change.getFlags() & FLAG_IS_WALLPAPER) != 0) { + hasWallpaper = true; if (TransitionUtil.isOpeningType(change.getMode())) { hasOpenWallpaper = true; } else if (TransitionUtil.isClosingType(change.getMode())) { @@ -965,6 +969,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { return WALLPAPER_TRANSITION_OPEN; } else if (hasCloseWallpaper) { return WALLPAPER_TRANSITION_CLOSE; + } else if (hasWallpaper) { + return WALLPAPER_TRANSITION_CHANGE; } else { return WALLPAPER_TRANSITION_NONE; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index d2760ff88ece..f6e38dac859c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -28,12 +28,15 @@ import static android.view.WindowManager.TRANSIT_SLEEP; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.fixScale; +import static android.window.TransitionInfo.FLAGS_IS_NON_APP_WINDOW; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; import static android.window.TransitionInfo.FLAG_IS_OCCLUDED; +import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_NO_ANIMATION; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; +import static com.android.window.flags.Flags.ensureWallpaperInTransitions; import static com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary; import static com.android.wm.shell.shared.TransitionUtil.isClosingType; import static com.android.wm.shell.shared.TransitionUtil.isOpeningType; @@ -519,12 +522,17 @@ public class Transitions implements RemoteCallable<Transitions>, boolean isOpening = isOpeningType(info.getType()); for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); - if (change.hasFlags(TransitionInfo.FLAGS_IS_NON_APP_WINDOW)) { + if (change.hasFlags(FLAGS_IS_NON_APP_WINDOW & ~FLAG_IS_WALLPAPER)) { // Currently system windows are controlled by WindowState, so don't change their // surfaces. Otherwise their surfaces could be hidden or cropped unexpectedly. - // This includes Wallpaper (always z-ordered at bottom) and IME (associated with - // app), because there may not be a transition associated with their visibility - // changes, and currently they don't need transition animation. + // This includes IME (associated with app), because there may not be a transition + // associated with their visibility changes, and currently they don't need a + // transition animation. + continue; + } + if (change.hasFlags(FLAG_IS_WALLPAPER) && !ensureWallpaperInTransitions()) { + // Wallpaper is always z-ordered at bottom, and historically is not animated by + // transition handlers. continue; } final SurfaceControl leash = change.getLeash(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java index c045cebdf4e0..a2d2b9aff597 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java @@ -27,6 +27,7 @@ import androidx.annotation.Nullable; import java.util.Arrays; import java.util.List; +import java.util.Set; /** * Simple container for recent tasks. May contain either a single or pair of tasks. @@ -50,6 +51,9 @@ public class GroupedRecentTaskInfo implements Parcelable { private final SplitBounds mSplitBounds; @GroupType private final int mType; + // TODO(b/348332802): move isMinimized inside each Task object instead once we have a + // replacement for RecentTaskInfo + private final int[] mMinimizedTaskIds; /** * Create new for a single task @@ -57,7 +61,7 @@ public class GroupedRecentTaskInfo implements Parcelable { public static GroupedRecentTaskInfo forSingleTask( @NonNull ActivityManager.RecentTaskInfo task) { return new GroupedRecentTaskInfo(new ActivityManager.RecentTaskInfo[]{task}, null, - TYPE_SINGLE); + TYPE_SINGLE, null /* minimizedFreeformTasks */); } /** @@ -66,28 +70,51 @@ public class GroupedRecentTaskInfo implements Parcelable { public static GroupedRecentTaskInfo forSplitTasks(@NonNull ActivityManager.RecentTaskInfo task1, @NonNull ActivityManager.RecentTaskInfo task2, @Nullable SplitBounds splitBounds) { return new GroupedRecentTaskInfo(new ActivityManager.RecentTaskInfo[]{task1, task2}, - splitBounds, TYPE_SPLIT); + splitBounds, TYPE_SPLIT, null /* minimizedFreeformTasks */); } /** * Create new for a group of freeform tasks */ public static GroupedRecentTaskInfo forFreeformTasks( - @NonNull ActivityManager.RecentTaskInfo... tasks) { - return new GroupedRecentTaskInfo(tasks, null, TYPE_FREEFORM); + @NonNull ActivityManager.RecentTaskInfo[] tasks, + @NonNull Set<Integer> minimizedFreeformTasks) { + return new GroupedRecentTaskInfo( + tasks, + null /* splitBounds */, + TYPE_FREEFORM, + minimizedFreeformTasks.stream().mapToInt(i -> i).toArray()); } - private GroupedRecentTaskInfo(@NonNull ActivityManager.RecentTaskInfo[] tasks, - @Nullable SplitBounds splitBounds, @GroupType int type) { + private GroupedRecentTaskInfo( + @NonNull ActivityManager.RecentTaskInfo[] tasks, + @Nullable SplitBounds splitBounds, + @GroupType int type, + @Nullable int[] minimizedFreeformTaskIds) { mTasks = tasks; mSplitBounds = splitBounds; mType = type; + mMinimizedTaskIds = minimizedFreeformTaskIds; + ensureAllMinimizedIdsPresent(tasks, minimizedFreeformTaskIds); + } + + private static void ensureAllMinimizedIdsPresent( + @NonNull ActivityManager.RecentTaskInfo[] tasks, + @Nullable int[] minimizedFreeformTaskIds) { + if (minimizedFreeformTaskIds == null) { + return; + } + if (!Arrays.stream(minimizedFreeformTaskIds).allMatch( + taskId -> Arrays.stream(tasks).anyMatch(task -> task.taskId == taskId))) { + throw new IllegalArgumentException("Minimized task IDs contain non-existent Task ID."); + } } GroupedRecentTaskInfo(Parcel parcel) { mTasks = parcel.createTypedArray(ActivityManager.RecentTaskInfo.CREATOR); mSplitBounds = parcel.readTypedObject(SplitBounds.CREATOR); mType = parcel.readInt(); + mMinimizedTaskIds = parcel.createIntArray(); } /** @@ -135,6 +162,10 @@ public class GroupedRecentTaskInfo implements Parcelable { return mType; } + public int[] getMinimizedTaskIds() { + return mMinimizedTaskIds; + } + @Override public String toString() { StringBuilder taskString = new StringBuilder(); @@ -161,6 +192,8 @@ public class GroupedRecentTaskInfo implements Parcelable { taskString.append("TYPE_FREEFORM"); break; } + taskString.append(", Minimized Task IDs: "); + taskString.append(Arrays.toString(mMinimizedTaskIds)); return taskString.toString(); } @@ -181,6 +214,7 @@ public class GroupedRecentTaskInfo implements Parcelable { parcel.writeTypedArray(mTasks, flags); parcel.writeTypedObject(mSplitBounds, flags); parcel.writeInt(mType); + parcel.writeIntArray(mMinimizedTaskIds); } @Override diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 14fa0f1a338d..0e53e10cde08 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager.RecentTaskInfo import android.app.ActivityManager.RunningTaskInfo +import android.app.KeyguardManager import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM @@ -149,6 +150,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Mock lateinit var syncQueue: SyncTransactionQueue @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock lateinit var transitions: Transitions + @Mock lateinit var keyguardManager: KeyguardManager @Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler @Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler @Mock @@ -233,6 +235,7 @@ class DesktopTasksControllerTest : ShellTestCase() { rootTaskDisplayAreaOrganizer, dragAndDropController, transitions, + keyguardManager, enterDesktopTransitionHandler, exitDesktopTransitionHandler, toggleResizeDesktopTaskTransitionHandler, @@ -1301,6 +1304,17 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun handleRequest_freeformTask_keyguardLocked_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + whenever(keyguardManager.isKeyguardLocked).thenReturn(true) + val freeformTask = createFreeformTask(displayId = DEFAULT_DISPLAY) + + val result = controller.handleRequest(Binder(), createTransition(freeformTask)) + + assertNull(result, "Should NOT handle request") + } + + @Test fun handleRequest_notOpenOrToFrontTransition_returnNull() { assumeTrue(ENABLE_SHELL_TRANSITIONS) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java index 974539f23b80..aa2d6f09508f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java @@ -241,16 +241,16 @@ public class TvPipGravityTest extends ShellTestCase { @Test public void updateGravity_move_expanded_valid() { - mTvPipBoundsState.setTvPipExpanded(true); - // Vertical expanded PiP. mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true); + mTvPipBoundsState.setTvPipExpanded(true); mTvPipBoundsState.setTvPipGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT); moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.CENTER_VERTICAL | Gravity.LEFT, true); moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.CENTER_VERTICAL | Gravity.RIGHT, true); // Horizontal expanded PiP. mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true); + mTvPipBoundsState.setTvPipExpanded(true); mTvPipBoundsState.setTvPipGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL); moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.TOP | Gravity.CENTER_HORIZONTAL, true); moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, true); @@ -281,10 +281,9 @@ public class TvPipGravityTest extends ShellTestCase { @Test public void updateGravity_move_expanded_invalid() { - mTvPipBoundsState.setTvPipExpanded(true); - // Vertical expanded PiP. mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true); + mTvPipBoundsState.setTvPipExpanded(true); mTvPipBoundsState.setTvPipGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT); moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.CENTER_VERTICAL | Gravity.RIGHT, false); moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.CENTER_VERTICAL | Gravity.RIGHT, false); @@ -297,6 +296,7 @@ public class TvPipGravityTest extends ShellTestCase { // Horizontal expanded PiP. mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true); + mTvPipBoundsState.setTvPipExpanded(true); mTvPipBoundsState.setTvPipGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL); moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, false); moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, false); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt index bbd65be9abda..15b73c541ed8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.recents import android.app.ActivityManager +import android.app.ActivityManager.RecentTaskInfo import android.graphics.Rect import android.os.Parcel import android.testing.AndroidTestingRunner @@ -33,6 +34,7 @@ import com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_SPLIT import com.android.wm.shell.util.SplitBounds import com.google.common.truth.Correspondence import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.mock @@ -86,12 +88,13 @@ class GroupedRecentTaskInfoTest : ShellTestCase() { @Test fun testFreeformTasks_hasCorrectType() { - assertThat(freeformTasksGroupInfo().type).isEqualTo(TYPE_FREEFORM) + assertThat(freeformTasksGroupInfo(freeformTaskIds = arrayOf(1)).type) + .isEqualTo(TYPE_FREEFORM) } @Test - fun testSplitTasks_taskInfoList_hasThreeTasks() { - val list = freeformTasksGroupInfo().taskInfoList + fun testCreateFreeformTasks_hasCorrectNumberOfTasks() { + val list = freeformTasksGroupInfo(freeformTaskIds = arrayOf(1, 2, 3)).taskInfoList assertThat(list).hasSize(3) assertThat(list[0].taskId).isEqualTo(1) assertThat(list[1].taskId).isEqualTo(2) @@ -99,6 +102,16 @@ class GroupedRecentTaskInfoTest : ShellTestCase() { } @Test + fun testCreateFreeformTasks_nonExistentMinimizedTaskId_throwsException() { + assertThrows(IllegalArgumentException::class.java) { + freeformTasksGroupInfo( + freeformTaskIds = arrayOf(1, 2, 3), + minimizedTaskIds = arrayOf(1, 4) + ) + } + } + + @Test fun testParcelling_singleTask() { val recentTaskInfo = singleTaskGroupInfo() val parcel = Parcel.obtain() @@ -129,7 +142,7 @@ class GroupedRecentTaskInfoTest : ShellTestCase() { @Test fun testParcelling_freeformTasks() { - val recentTaskInfo = freeformTasksGroupInfo() + val recentTaskInfo = freeformTasksGroupInfo(freeformTaskIds = arrayOf(1, 2, 3)) val parcel = Parcel.obtain() recentTaskInfo.writeToParcel(parcel, 0) parcel.setDataPosition(0) @@ -145,6 +158,21 @@ class GroupedRecentTaskInfoTest : ShellTestCase() { .containsExactly(1, 2, 3) } + @Test + fun testParcelling_freeformTasks_minimizedTasks() { + val recentTaskInfo = freeformTasksGroupInfo( + freeformTaskIds = arrayOf(1, 2, 3), minimizedTaskIds = arrayOf(2)) + + val parcel = Parcel.obtain() + recentTaskInfo.writeToParcel(parcel, 0) + parcel.setDataPosition(0) + + // Read the object back from the parcel + val recentTaskInfoParcel = CREATOR.createFromParcel(parcel) + assertThat(recentTaskInfoParcel.type).isEqualTo(TYPE_FREEFORM) + assertThat(recentTaskInfoParcel.minimizedTaskIds).isEqualTo(arrayOf(2).toIntArray()) + } + private fun createTaskInfo(id: Int) = ActivityManager.RecentTaskInfo().apply { taskId = id token = WindowContainerToken(mock(IWindowContainerToken::class.java)) @@ -162,10 +190,12 @@ class GroupedRecentTaskInfoTest : ShellTestCase() { return GroupedRecentTaskInfo.forSplitTasks(task1, task2, splitBounds) } - private fun freeformTasksGroupInfo(): GroupedRecentTaskInfo { - val task1 = createTaskInfo(id = 1) - val task2 = createTaskInfo(id = 2) - val task3 = createTaskInfo(id = 3) - return GroupedRecentTaskInfo.forFreeformTasks(task1, task2, task3) + private fun freeformTasksGroupInfo( + freeformTaskIds: Array<Int>, + minimizedTaskIds: Array<Int> = emptyArray() + ): GroupedRecentTaskInfo { + return GroupedRecentTaskInfo.forFreeformTasks( + freeformTaskIds.map { createTaskInfo(it) }.toTypedArray(), + minimizedTaskIds.toSet()) } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt index f9599702e763..0e5efa650cc4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt @@ -48,7 +48,6 @@ import org.mockito.kotlin.same import org.mockito.kotlin.verify import org.mockito.kotlin.whenever - /** * Test class for {@link TaskStackTransitionObserver} * @@ -168,6 +167,80 @@ class TaskStackTransitionObserverTest { .isEqualTo(freeformOpenChange.taskInfo?.windowingMode) } + @Test + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + fun transitionMerged_withChange_onlyOpenChangeIsNotified() { + val listener = TestListener() + val executor = TestShellExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + + // Create open transition + val change = + createChange( + WindowManager.TRANSIT_OPEN, + createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM) + ) + val transitionInfo = + TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build() + + // create change transition to be merged to above transition + val mergedChange = + createChange( + WindowManager.TRANSIT_CHANGE, + createTaskInfo(2, WindowConfiguration.WINDOWING_MODE_FREEFORM) + ) + val mergedTransitionInfo = + TransitionInfoBuilder(WindowManager.TRANSIT_CHANGE, 0).addChange(mergedChange).build() + val mergedTransition = Mockito.mock(IBinder::class.java) + + callOnTransitionReady(transitionInfo) + callOnTransitionReady(mergedTransitionInfo, mergedTransition) + callOnTransitionMerged(mergedTransition) + callOnTransitionFinished() + executor.flushAll() + + assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(change.taskInfo?.taskId) + assertThat(listener.taskInfoToBeNotified.windowingMode) + .isEqualTo(change.taskInfo?.windowingMode) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + fun transitionMerged_withOpen_lastOpenChangeIsNotified() { + val listener = TestListener() + val executor = TestShellExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + + // Create open transition + val change = + createChange( + WindowManager.TRANSIT_OPEN, + createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM) + ) + val transitionInfo = + TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build() + + // create change transition to be merged to above transition + val mergedChange = + createChange( + WindowManager.TRANSIT_OPEN, + createTaskInfo(2, WindowConfiguration.WINDOWING_MODE_FREEFORM) + ) + val mergedTransitionInfo = + TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(mergedChange).build() + val mergedTransition = Mockito.mock(IBinder::class.java) + + callOnTransitionReady(transitionInfo) + callOnTransitionReady(mergedTransitionInfo, mergedTransition) + callOnTransitionMerged(mergedTransition) + callOnTransitionFinished() + executor.flushAll() + + assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(mergedChange.taskInfo?.taskId) + assertThat(listener.taskInfoToBeNotified.windowingMode) + .isEqualTo(mergedChange.taskInfo?.windowingMode) + } + class TestListener : TaskStackTransitionObserver.TaskStackTransitionObserverListener { var taskInfoToBeNotified = ActivityManager.RunningTaskInfo() @@ -179,11 +252,14 @@ class TaskStackTransitionObserverTest { } /** Simulate calling the onTransitionReady() method */ - private fun callOnTransitionReady(transitionInfo: TransitionInfo) { + private fun callOnTransitionReady( + transitionInfo: TransitionInfo, + transition: IBinder = mockTransitionBinder + ) { val startT = Mockito.mock(SurfaceControl.Transaction::class.java) val finishT = Mockito.mock(SurfaceControl.Transaction::class.java) - transitionObserver.onTransitionReady(mockTransitionBinder, transitionInfo, startT, finishT) + transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT) } /** Simulate calling the onTransitionFinished() method */ @@ -191,6 +267,11 @@ class TaskStackTransitionObserverTest { transitionObserver.onTransitionFinished(mockTransitionBinder, false) } + /** Simulate calling the onTransitionMerged() method */ + private fun callOnTransitionMerged(merged: IBinder, playing: IBinder = mockTransitionBinder) { + transitionObserver.onTransitionMerged(merged, playing) + } + companion object { fun createTaskInfo(taskId: Int, windowingMode: Int): ActivityManager.RunningTaskInfo { val taskInfo = ActivityManager.RunningTaskInfo() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ChangeBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ChangeBuilder.java new file mode 100644 index 000000000000..b54c3bf72110 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ChangeBuilder.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.transition; + +import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; + +import static org.mockito.Mockito.mock; + +import android.app.ActivityManager.RunningTaskInfo; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.TransitionInfo; + +public class ChangeBuilder { + final TransitionInfo.Change mChange; + + ChangeBuilder(@WindowManager.TransitionType int mode) { + mChange = new TransitionInfo.Change(null /* token */, createMockSurface(true)); + mChange.setMode(mode); + } + + ChangeBuilder setFlags(@TransitionInfo.ChangeFlags int flags) { + mChange.setFlags(flags); + return this; + } + + ChangeBuilder setTask(RunningTaskInfo taskInfo) { + mChange.setTaskInfo(taskInfo); + return this; + } + + ChangeBuilder setRotate(int anim) { + return setRotate(Surface.ROTATION_90, anim); + } + + ChangeBuilder setRotate() { + return setRotate(ROTATION_ANIMATION_UNSPECIFIED); + } + + ChangeBuilder setRotate(@Surface.Rotation int target, int anim) { + mChange.setRotation(Surface.ROTATION_0, target); + mChange.setRotationAnimation(anim); + return this; + } + + TransitionInfo.Change build() { + return mChange; + } + + private static SurfaceControl createMockSurface(boolean valid) { + SurfaceControl sc = mock(SurfaceControl.class); + doReturn(valid).when(sc).isValid(); + return sc; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java new file mode 100644 index 000000000000..754a173ff069 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.transition; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_SLEEP; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.window.TransitionInfo.FLAG_SYNC; +import static android.window.TransitionInfo.FLAG_TRANSLUCENT; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.WindowContainerToken; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.sysui.ShellInit; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for the default animation handler that is used if no other special-purpose handler picks + * up an animation request. + * + * Build/Install/Run: + * atest WMShellUnitTests:DefaultTransitionHandlerTest + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class DefaultTransitionHandlerTest extends ShellTestCase { + + private final Context mContext = + InstrumentationRegistry.getInstrumentation().getTargetContext(); + + private final DisplayController mDisplayController = mock(DisplayController.class); + private final TransactionPool mTransactionPool = new MockTransactionPool(); + private final TestShellExecutor mMainExecutor = new TestShellExecutor(); + private final TestShellExecutor mAnimExecutor = new TestShellExecutor(); + private final Handler mMainHandler = new Handler(Looper.getMainLooper()); + + private ShellInit mShellInit; + private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; + private DefaultTransitionHandler mTransitionHandler; + + @Before + public void setUp() { + mShellInit = new ShellInit(mMainExecutor); + mRootTaskDisplayAreaOrganizer = new RootTaskDisplayAreaOrganizer( + mMainExecutor, + mContext, + mShellInit); + mTransitionHandler = new DefaultTransitionHandler( + mContext, mShellInit, mDisplayController, + mTransactionPool, mMainExecutor, mMainHandler, mAnimExecutor, + mRootTaskDisplayAreaOrganizer); + mShellInit.init(); + } + + @After + public void tearDown() { + flushHandlers(); + } + + private void flushHandlers() { + mMainHandler.runWithScissors(() -> { + mAnimExecutor.flushAll(); + mMainExecutor.flushAll(); + }, 1000L); + } + + @Test + public void testAnimationBackgroundCreatedForTaskTransition() { + final TransitionInfo.Change openTask = new ChangeBuilder(TRANSIT_OPEN) + .setTask(createTaskInfo(1)) + .build(); + final TransitionInfo.Change closeTask = new ChangeBuilder(TRANSIT_TO_BACK) + .setTask(createTaskInfo(2)) + .build(); + + final IBinder token = new Binder(); + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(openTask) + .addChange(closeTask) + .build(); + final SurfaceControl.Transaction startT = MockTransactionPool.create(); + final SurfaceControl.Transaction finishT = MockTransactionPool.create(); + + mTransitionHandler.startAnimation(token, info, startT, finishT, + mock(Transitions.TransitionFinishCallback.class)); + + mergeSync(mTransitionHandler, token); + flushHandlers(); + + verify(startT).setColor(any(), any()); + } + + @Test + public void testNoAnimationBackgroundForTranslucentTasks() { + final TransitionInfo.Change openTask = new ChangeBuilder(TRANSIT_OPEN) + .setTask(createTaskInfo(1)) + .setFlags(FLAG_TRANSLUCENT) + .build(); + final TransitionInfo.Change closeTask = new ChangeBuilder(TRANSIT_TO_BACK) + .setTask(createTaskInfo(2)) + .setFlags(FLAG_TRANSLUCENT) + .build(); + + final IBinder token = new Binder(); + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(openTask) + .addChange(closeTask) + .build(); + final SurfaceControl.Transaction startT = MockTransactionPool.create(); + final SurfaceControl.Transaction finishT = MockTransactionPool.create(); + + mTransitionHandler.startAnimation(token, info, startT, finishT, + mock(Transitions.TransitionFinishCallback.class)); + + mergeSync(mTransitionHandler, token); + flushHandlers(); + + verify(startT, never()).setColor(any(), any()); + } + + @Test + public void testNoAnimationBackgroundForWallpapers() { + final TransitionInfo.Change openWallpaper = new ChangeBuilder(TRANSIT_OPEN) + .setFlags(TransitionInfo.FLAG_IS_WALLPAPER) + .build(); + final TransitionInfo.Change closeWallpaper = new ChangeBuilder(TRANSIT_TO_BACK) + .setFlags(TransitionInfo.FLAG_IS_WALLPAPER) + .build(); + + final IBinder token = new Binder(); + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(openWallpaper) + .addChange(closeWallpaper) + .build(); + final SurfaceControl.Transaction startT = MockTransactionPool.create(); + final SurfaceControl.Transaction finishT = MockTransactionPool.create(); + + mTransitionHandler.startAnimation(token, info, startT, finishT, + mock(Transitions.TransitionFinishCallback.class)); + + mergeSync(mTransitionHandler, token); + flushHandlers(); + + verify(startT, never()).setColor(any(), any()); + } + + private static void mergeSync(Transitions.TransitionHandler handler, IBinder token) { + handler.mergeAnimation( + new Binder(), + new TransitionInfoBuilder(TRANSIT_SLEEP, FLAG_SYNC).build(), + MockTransactionPool.create(), + token, + mock(Transitions.TransitionFinishCallback.class)); + } + + private static RunningTaskInfo createTaskInfo(int taskId) { + RunningTaskInfo taskInfo = new RunningTaskInfo(); + taskInfo.taskId = taskId; + taskInfo.topActivityType = ACTIVITY_TYPE_STANDARD; + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + taskInfo.configuration.windowConfiguration.setActivityType(taskInfo.topActivityType); + taskInfo.token = mock(WindowContainerToken.class); + return taskInfo; + } +} + diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/MockTransactionPool.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/MockTransactionPool.java new file mode 100644 index 000000000000..574a87ac4b17 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/MockTransactionPool.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.transition; + +import static org.mockito.Mockito.RETURNS_SELF; +import static org.mockito.Mockito.mock; + +import android.view.SurfaceControl; + +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.util.StubTransaction; + +public class MockTransactionPool extends TransactionPool { + + public static SurfaceControl.Transaction create() { + return mock(StubTransaction.class, RETURNS_SELF); + } + + @Override + public SurfaceControl.Transaction acquire() { + return create(); + } + + @Override + public void release(SurfaceControl.Transaction t) { + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index 69a61eadf61d..8331d591fd59 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -79,7 +79,6 @@ import android.util.Pair; import android.view.IRecentsAnimationRunner; import android.view.Surface; import android.view.SurfaceControl; -import android.view.WindowManager; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; import android.window.IWindowContainerToken; @@ -1615,43 +1614,6 @@ public class ShellTransitionTests extends ShellTestCase { eq(R.styleable.WindowAnimation_activityCloseEnterAnimation), anyBoolean()); } - class ChangeBuilder { - final TransitionInfo.Change mChange; - - ChangeBuilder(@WindowManager.TransitionType int mode) { - mChange = new TransitionInfo.Change(null /* token */, createMockSurface(true)); - mChange.setMode(mode); - } - - ChangeBuilder setFlags(@TransitionInfo.ChangeFlags int flags) { - mChange.setFlags(flags); - return this; - } - - ChangeBuilder setTask(RunningTaskInfo taskInfo) { - mChange.setTaskInfo(taskInfo); - return this; - } - - ChangeBuilder setRotate(int anim) { - return setRotate(Surface.ROTATION_90, anim); - } - - ChangeBuilder setRotate() { - return setRotate(ROTATION_ANIMATION_UNSPECIFIED); - } - - ChangeBuilder setRotate(@Surface.Rotation int target, int anim) { - mChange.setRotation(Surface.ROTATION_0, target); - mChange.setRotationAnimation(anim); - return this; - } - - TransitionInfo.Change build() { - return mChange; - } - } - class TestTransitionHandler implements Transitions.TransitionHandler { ArrayList<Pair<IBinder, Transitions.TransitionFinishCallback>> mFinishes = new ArrayList<>(); @@ -1740,12 +1702,6 @@ public class ShellTransitionTests extends ShellTestCase { .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); } - private static SurfaceControl createMockSurface(boolean valid) { - SurfaceControl sc = mock(SurfaceControl.class); - doReturn(valid).when(sc).isValid(); - return sc; - } - private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode, int activityType) { RunningTaskInfo taskInfo = new RunningTaskInfo(); taskInfo.taskId = taskId; diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp index 77800a305f02..2fff4f5e9f7c 100644 --- a/libs/androidfw/Android.bp +++ b/libs/androidfw/Android.bp @@ -212,6 +212,7 @@ cc_test { "tests/AttributeResolution_test.cpp", "tests/BigBuffer_test.cpp", "tests/ByteBucketArray_test.cpp", + "tests/CombinedIterator_test.cpp", "tests/Config_test.cpp", "tests/ConfigDescription_test.cpp", "tests/ConfigLocale_test.cpp", @@ -267,6 +268,7 @@ cc_test { cc_benchmark { name: "libandroidfw_benchmarks", defaults: ["libandroidfw_defaults"], + test_config: "tests/AndroidTest_Benchmarks.xml", srcs: [ // Helpers/infra for benchmarking. "tests/BenchMain.cpp", @@ -282,7 +284,11 @@ cc_benchmark { "tests/Theme_bench.cpp", ], shared_libs: common_test_libs, - data: ["tests/data/**/*.apk"], + data: [ + "tests/data/**/*.apk", + ":FrameworkResourcesSparseTestApp", + ":FrameworkResourcesNotSparseTestApp", + ], } cc_library { diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index 46f636e2ae7f..822a387351e3 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -23,9 +23,11 @@ #include <map> #include <set> #include <span> +#include <utility> #include "android-base/logging.h" #include "android-base/stringprintf.h" +#include "androidfw/CombinedIterator.h" #include "androidfw/ResourceTypes.h" #include "androidfw/ResourceUtils.h" #include "androidfw/Util.h" @@ -1622,6 +1624,12 @@ Theme::Theme(AssetManager2* asset_manager) : asset_manager_(asset_manager) { Theme::~Theme() = default; +static bool IsUndefined(const Res_value& value) { + // DATA_NULL_EMPTY (@empty) is a valid resource value and DATA_NULL_UNDEFINED represents + // an absence of a valid value. + return value.dataType == Res_value::TYPE_NULL && value.data != Res_value::DATA_NULL_EMPTY; +} + base::expected<std::monostate, NullOrIOError> Theme::ApplyStyle(uint32_t resid, bool force) { ATRACE_NAME("Theme::ApplyStyle"); @@ -1633,39 +1641,76 @@ base::expected<std::monostate, NullOrIOError> Theme::ApplyStyle(uint32_t resid, // Merge the flags from this style. type_spec_flags_ |= (*bag)->type_spec_flags; + // + // This function is the most expensive part of applying an frro to the existing app resources, + // and needs to be as efficient as possible. + // The data structure we're working with is two parallel sorted arrays of keys (resource IDs) + // and entries (resource value + some attributes). + // The styles get applied in sequence, starting with an empty set of attributes. Each style + // contains its values for the theme attributes, and gets applied in either normal or forced way: + // - normal way never overrides the existing attribute, so only unique style attributes are added + // - forced way overrides anything for that attribute, and if it's undefined it removes the + // previous value completely + // + // Style attributes come in a Bag data type - a sorted array of attributes with their values. This + // means we don't need to re-sort the attributes ever, and instead: + // - for an already existing attribute just skip it or apply the forced value + // - if the forced value is undefined, mark it undefined as well to get rid of it later + // - for a new attribute append it to the array, forming a new sorted section of new attributes + // past the end of the original ones (ignore undefined ones here) + // - inplace merge two sorted sections to form a single sorted array again. + // - run the last pass to remove all undefined elements + // + // Using this algorithm performs better than a repeated binary search + insert in the middle, + // as that keeps shifting the tail end of the arrays and wasting CPU cycles in memcpy(). + // + const auto starting_size = keys_.size(); + if (starting_size == 0) { + keys_.reserve((*bag)->entry_count); + entries_.reserve((*bag)->entry_count); + } + bool wrote_undefined = false; for (auto it = begin(*bag); it != end(*bag); ++it) { const uint32_t attr_res_id = it->key; - // If the resource ID passed in is not a style, the key can be some other identifier that is not // a resource ID. We should fail fast instead of operating with strange resource IDs. if (!is_valid_resid(attr_res_id)) { return base::unexpected(std::nullopt); } - - // DATA_NULL_EMPTY (@empty) is a valid resource value and DATA_NULL_UNDEFINED represents - // an absence of a valid value. - bool is_undefined = it->value.dataType == Res_value::TYPE_NULL && - it->value.data != Res_value::DATA_NULL_EMPTY; + const bool is_undefined = IsUndefined(it->value); if (!force && is_undefined) { continue; } - - const auto key_it = std::lower_bound(keys_.begin(), keys_.end(), attr_res_id); - const auto entry_it = entries_.begin() + (key_it - keys_.begin()); - if (key_it != keys_.end() && *key_it == attr_res_id) { - if (is_undefined) { - // DATA_NULL_UNDEFINED clears the value of the attribute in the theme only when `force` is - // true. - keys_.erase(key_it); - entries_.erase(entry_it); - } else if (force) { + const auto key_it = std::lower_bound(keys_.begin(), keys_.begin() + starting_size, attr_res_id); + if (key_it != keys_.begin() + starting_size && *key_it == attr_res_id) { + const auto entry_it = entries_.begin() + (key_it - keys_.begin()); + if (force || IsUndefined(entry_it->value)) { *entry_it = Entry{it->cookie, (*bag)->type_spec_flags, it->value}; + wrote_undefined |= is_undefined; } - } else { - keys_.insert(key_it, attr_res_id); - entries_.insert(entry_it, Entry{it->cookie, (*bag)->type_spec_flags, it->value}); + } else if (!is_undefined) { + keys_.emplace_back(attr_res_id); + entries_.emplace_back(it->cookie, (*bag)->type_spec_flags, it->value); } } + + if (starting_size && keys_.size() != starting_size) { + std::inplace_merge( + CombinedIterator(keys_.begin(), entries_.begin()), + CombinedIterator(keys_.begin() + starting_size, entries_.begin() + starting_size), + CombinedIterator(keys_.end(), entries_.end())); + } + if (wrote_undefined) { + auto new_end = std::remove_if(CombinedIterator(keys_.begin(), entries_.begin()), + CombinedIterator(keys_.end(), entries_.end()), + [](const auto& pair) { return IsUndefined(pair.second.value); }); + keys_.erase(new_end.it1, keys_.end()); + entries_.erase(new_end.it2, entries_.end()); + } + if (android::base::kEnableDChecks && !std::is_sorted(keys_.begin(), keys_.end())) { + ALOGW("Bag %u was unsorted in the apk?", unsigned(resid)); + return base::unexpected(std::nullopt); + } return {}; } @@ -1691,6 +1736,9 @@ std::optional<AssetManager2::SelectedValue> Theme::GetAttribute(uint32_t resid) return std::nullopt; } const auto entry_it = entries_.begin() + (key_it - keys_.begin()); + if (IsUndefined(entry_it->value)) { + return std::nullopt; + } type_spec_flags |= entry_it->type_spec_flags; if (entry_it->value.dataType == Res_value::TYPE_ATTRIBUTE) { resid = entry_it->value.data; diff --git a/libs/androidfw/include/androidfw/AssetManager2.h b/libs/androidfw/include/androidfw/AssetManager2.h index 17a8ba6c03bd..ac46bc5c179f 100644 --- a/libs/androidfw/include/androidfw/AssetManager2.h +++ b/libs/androidfw/include/androidfw/AssetManager2.h @@ -280,9 +280,9 @@ class AssetManager2 { private: SelectedValue(uint8_t value_type, Res_value::data_type value_data, ApkAssetsCookie cookie, - uint32_t type_flags, uint32_t resid, const ResTable_config& config) : + uint32_t type_flags, uint32_t resid, ResTable_config config) : cookie(cookie), data(value_data), type(value_type), flags(type_flags), - resid(resid), config(config) {}; + resid(resid), config(std::move(config)) {} }; // Retrieves the best matching resource value with ID `resid`. diff --git a/libs/androidfw/include/androidfw/CombinedIterator.h b/libs/androidfw/include/androidfw/CombinedIterator.h new file mode 100644 index 000000000000..4ff6a7d7e6c9 --- /dev/null +++ b/libs/androidfw/include/androidfw/CombinedIterator.h @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <compare> +#include <iterator> +#include <utility> + +namespace android { + +namespace detail { +// A few useful aliases to not repeat them everywhere +template <class It1, class It2> +using Value = std::pair<typename std::iterator_traits<It1>::value_type, + typename std::iterator_traits<It2>::value_type>; + +template <class It1, class It2> +using BaseRefPair = std::pair<typename std::iterator_traits<It1>::reference, + typename std::iterator_traits<It2>::reference>; + +template <class It1, class It2> +struct RefPair : BaseRefPair<It1, It2> { + using Base = BaseRefPair<It1, It2>; + using Value = detail::Value<It1, It2>; + + RefPair(It1 it1, It2 it2) : Base(*it1, *it2) { + } + + RefPair& operator=(const Value& v) { + this->first = v.first; + this->second = v.second; + return *this; + } + operator Value() const { + return Value(this->first, this->second); + } + bool operator==(const RefPair& other) { + return this->first == other.first; + } + bool operator==(const Value& other) { + return this->first == other.first; + } + std::strong_ordering operator<=>(const RefPair& other) const { + return this->first <=> other.first; + } + std::strong_ordering operator<=>(const Value& other) const { + return this->first <=> other.first; + } + friend void swap(RefPair& l, RefPair& r) { + using std::swap; + swap(l.first, r.first); + swap(l.second, r.second); + } +}; + +template <class It1, class It2> +struct RefPairPtr { + RefPair<It1, It2> value; + + RefPair<It1, It2>* operator->() const { + return &value; + } +}; +} // namespace detail + +// +// CombinedIterator - a class to combine two iterators to process them as a single iterator to a +// pair of values. Useful for processing a data structure of "struct of arrays", replacing +// array of structs for cache locality. +// +// The value type is a pair of copies of the values of each iterator, and the reference is a +// pair of references to the corresponding values. Comparison only compares the first element, +// making it most useful for using on data like (vector<Key>, vector<Value>) for binary searching, +// sorting both together and so on. +// +// The class is designed for handling arrays, so it requires random access iterators as an input. +// + +template <class It1, class It2> +requires std::random_access_iterator<It1> && std::random_access_iterator<It2> +struct CombinedIterator { + typedef detail::Value<It1, It2> value_type; + typedef detail::RefPair<It1, It2> reference; + typedef std::ptrdiff_t difference_type; + typedef detail::RefPairPtr<It1, It2> pointer; + typedef std::random_access_iterator_tag iterator_category; + + CombinedIterator(It1 it1 = {}, It2 it2 = {}) : it1(it1), it2(it2) { + } + + bool operator<(const CombinedIterator& other) const { + return it1 < other.it1; + } + bool operator<=(const CombinedIterator& other) const { + return it1 <= other.it1; + } + bool operator>(const CombinedIterator& other) const { + return it1 > other.it1; + } + bool operator>=(const CombinedIterator& other) const { + return it1 >= other.it1; + } + bool operator==(const CombinedIterator& other) const { + return it1 == other.it1; + } + pointer operator->() const { + return pointer{{it1, it2}}; + } + reference operator*() const { + return {it1, it2}; + } + reference operator[](difference_type n) const { + return {it1 + n, it2 + n}; + } + + CombinedIterator& operator++() { + ++it1; + ++it2; + return *this; + } + CombinedIterator operator++(int) { + const auto res = *this; + ++*this; + return res; + } + CombinedIterator& operator--() { + --it1; + --it2; + return *this; + } + CombinedIterator operator--(int) { + const auto res = *this; + --*this; + return res; + } + CombinedIterator& operator+=(difference_type n) { + it1 += n; + it2 += n; + return *this; + } + CombinedIterator operator+(difference_type n) const { + CombinedIterator res = *this; + return res += n; + } + + CombinedIterator& operator-=(difference_type n) { + it1 -= n; + it2 -= n; + return *this; + } + CombinedIterator operator-(difference_type n) const { + CombinedIterator res = *this; + return res -= n; + } + difference_type operator-(const CombinedIterator& other) { + return it1 - other.it1; + } + + It1 it1; + It2 it2; +}; + +} // namespace android diff --git a/libs/androidfw/tests/AndroidTest_Benchmarks.xml b/libs/androidfw/tests/AndroidTest_Benchmarks.xml new file mode 100644 index 000000000000..e61e46fb7785 --- /dev/null +++ b/libs/androidfw/tests/AndroidTest_Benchmarks.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="Runs libandroidfw_benchmarks and libandroidfw_tests."> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="apct-native-metric" /> + + <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> + <option name="cleanup" value="true" /> + <option name="push" value="libandroidfw_benchmarks->/data/local/tmp/libandroidfw_benchmarks" /> + </target_preparer> + <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" > + <option name="native-benchmark-device-path" value="/data/local/tmp" /> + <option name="benchmark-module-name" value="libandroidfw_benchmarks" /> + <!-- The GoogleBenchmarkTest class ordinarily expects every file in the benchmark's + directory (recursively) to be a google-benchmark binary, so we need this setting to + avoid failing on the test data files. --> + <option name="file-exclusion-filter-regex" value=".*\.(apk|config)$" /> + </test> +</configuration>
\ No newline at end of file diff --git a/libs/androidfw/tests/AssetManager2_bench.cpp b/libs/androidfw/tests/AssetManager2_bench.cpp index 2caa98c35971..136f5ea639a1 100644 --- a/libs/androidfw/tests/AssetManager2_bench.cpp +++ b/libs/androidfw/tests/AssetManager2_bench.cpp @@ -37,7 +37,7 @@ constexpr const static char* kFrameworkPath = "/system/framework/framework-res.a static void BM_AssetManagerLoadAssets(benchmark::State& state) { std::string path = GetTestDataPath() + "/basic/basic.apk"; - while (state.KeepRunning()) { + for (auto&& _ : state) { auto apk = ApkAssets::Load(path); AssetManager2 assets; assets.SetApkAssets({apk}); @@ -47,7 +47,7 @@ BENCHMARK(BM_AssetManagerLoadAssets); static void BM_AssetManagerLoadAssetsOld(benchmark::State& state) { String8 path((GetTestDataPath() + "/basic/basic.apk").data()); - while (state.KeepRunning()) { + for (auto&& _ : state) { AssetManager assets; assets.addAssetPath(path, nullptr /* cookie */, false /* appAsLib */, false /* isSystemAsset */); @@ -60,7 +60,7 @@ BENCHMARK(BM_AssetManagerLoadAssetsOld); static void BM_AssetManagerLoadFrameworkAssets(benchmark::State& state) { std::string path = kFrameworkPath; - while (state.KeepRunning()) { + for (auto&& _ : state) { auto apk = ApkAssets::Load(path); AssetManager2 assets; assets.SetApkAssets({apk}); @@ -70,7 +70,7 @@ BENCHMARK(BM_AssetManagerLoadFrameworkAssets); static void BM_AssetManagerLoadFrameworkAssetsOld(benchmark::State& state) { String8 path(kFrameworkPath); - while (state.KeepRunning()) { + for (auto&& _ : state) { AssetManager assets; assets.addAssetPath(path, nullptr /* cookie */, false /* appAsLib */, false /* isSystemAsset */); @@ -138,7 +138,7 @@ static void BM_AssetManagerGetBag(benchmark::State& state) { AssetManager2 assets; assets.SetApkAssets({apk}); - while (state.KeepRunning()) { + for (auto&& _ : state) { auto bag = assets.GetBag(app::R::style::StyleTwo); if (!bag.has_value()) { state.SkipWithError("Failed to load get bag"); @@ -165,7 +165,7 @@ static void BM_AssetManagerGetBagOld(benchmark::State& state) { const ResTable& table = assets.getResources(true); - while (state.KeepRunning()) { + for (auto&& _ : state) { const ResTable::bag_entry* bag_begin; const ssize_t N = table.lockBag(app::R::style::StyleTwo, &bag_begin); const ResTable::bag_entry* const bag_end = bag_begin + N; @@ -190,7 +190,7 @@ static void BM_AssetManagerGetResourceLocales(benchmark::State& state) { AssetManager2 assets; assets.SetApkAssets({apk}); - while (state.KeepRunning()) { + for (auto&& _ : state) { std::set<std::string> locales = assets.GetResourceLocales(false /*exclude_system*/, true /*merge_equivalent_languages*/); benchmark::DoNotOptimize(locales); @@ -208,7 +208,7 @@ static void BM_AssetManagerGetResourceLocalesOld(benchmark::State& state) { const ResTable& table = assets.getResources(true); - while (state.KeepRunning()) { + for (auto&& _ : state) { Vector<String8> locales; table.getLocales(&locales, true /*includeSystemLocales*/, true /*mergeEquivalentLangs*/); benchmark::DoNotOptimize(locales); @@ -231,7 +231,7 @@ static void BM_AssetManagerSetConfigurationFramework(benchmark::State& state) { std::vector<ResTable_config> configs; configs.push_back(config); - while (state.KeepRunning()) { + for (auto&& _ : state) { configs[0].sdkVersion = ~configs[0].sdkVersion; assets.SetConfigurations(configs); } @@ -251,7 +251,7 @@ static void BM_AssetManagerSetConfigurationFrameworkOld(benchmark::State& state) ResTable_config config; memset(&config, 0, sizeof(config)); - while (state.KeepRunning()) { + for (auto&& _ : state) { config.sdkVersion = ~config.sdkVersion; assets.setConfiguration(config); } diff --git a/libs/androidfw/tests/BenchmarkHelpers.cpp b/libs/androidfw/tests/BenchmarkHelpers.cpp index 8b883f4ed1df..e3fc0a0a4e68 100644 --- a/libs/androidfw/tests/BenchmarkHelpers.cpp +++ b/libs/androidfw/tests/BenchmarkHelpers.cpp @@ -28,7 +28,7 @@ void GetResourceBenchmarkOld(const std::vector<std::string>& paths, const ResTab for (const std::string& path : paths) { if (!assetmanager.addAssetPath(String8(path.c_str()), nullptr /* cookie */, false /* appAsLib */, false /* isSystemAssets */)) { - state.SkipWithError(base::StringPrintf("Failed to load assets %s", path.c_str()).c_str()); + state.SkipWithError(base::StringPrintf("Failed to old-load assets %s", path.c_str()).c_str()); return; } } @@ -57,7 +57,7 @@ void GetResourceBenchmark(const std::vector<std::string>& paths, const ResTable_ for (const std::string& path : paths) { auto apk = ApkAssets::Load(path); if (apk == nullptr) { - state.SkipWithError(base::StringPrintf("Failed to load assets %s", path.c_str()).c_str()); + state.SkipWithError(base::StringPrintf("Failed to new-load assets %s", path.c_str()).c_str()); return; } apk_assets.push_back(std::move(apk)); diff --git a/libs/androidfw/tests/CombinedIterator_test.cpp b/libs/androidfw/tests/CombinedIterator_test.cpp new file mode 100644 index 000000000000..c1228f34625f --- /dev/null +++ b/libs/androidfw/tests/CombinedIterator_test.cpp @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "androidfw/CombinedIterator.h" + +#include <algorithm> +#include <string> +#include <strstream> +#include <utility> +#include <vector> + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace android { + +template <class Coll> +std::string toString(const Coll& coll) { + std::stringstream res; + res << "(" << std::size(coll) << ")"; + if (std::size(coll)) { + res << "{" << coll[0]; + for (int i = 1; i != std::size(coll); ++i) { + res << "," << coll[i]; + } + res << "}"; + } + return res.str(); +} + +template <class Coll> +void AssertCollectionEq(const Coll& first, const Coll& second) { + ASSERT_EQ(std::size(first), std::size(second)) + << "first: " << toString(first) << ", second: " << toString(second); + for (int i = 0; i != std::size(first); ++i) { + ASSERT_EQ(first[i], second[i]) + << "index: " << i << " first: " << toString(first) << ", second: " << toString(second); + } +} + +TEST(CombinedIteratorTest, Sorting) { + std::vector<int> v1 = {2, 1, 3, 4, 0}; + std::vector<int> v2 = {20, 10, 30, 40, 0}; + + std::sort(CombinedIterator(v1.begin(), v2.begin()), CombinedIterator(v1.end(), v2.end())); + + ASSERT_EQ(v1.size(), v2.size()); + ASSERT_TRUE(std::is_sorted(v1.begin(), v1.end())); + ASSERT_TRUE(std::is_sorted(v2.begin(), v2.end())); + AssertCollectionEq(v1, {0, 1, 2, 3, 4}); + AssertCollectionEq(v2, {0, 10, 20, 30, 40}); +} + +TEST(CombinedIteratorTest, Removing) { + std::vector<int> v1 = {1, 2, 3, 4, 5, 5, 5, 6}; + std::vector<int> v2 = {10, 20, 30, 40, 50, 50, 50, 60}; + + auto newEnd = + std::remove_if(CombinedIterator(v1.begin(), v2.begin()), CombinedIterator(v1.end(), v2.end()), + [](auto&& pair) { return pair.first >= 3 && pair.first <= 5; }); + + ASSERT_EQ(newEnd.it1, v1.begin() + 3); + ASSERT_EQ(newEnd.it2, v2.begin() + 3); + + v1.erase(newEnd.it1, v1.end()); + AssertCollectionEq(v1, {1, 2, 6}); + v2.erase(newEnd.it2, v2.end()); + AssertCollectionEq(v2, {10, 20, 60}); +} + +TEST(CombinedIteratorTest, InplaceMerge) { + std::vector<int> v1 = {1, 3, 4, 7, 2, 5, 6}; + std::vector<int> v2 = {10, 30, 40, 70, 20, 50, 60}; + + std::inplace_merge(CombinedIterator(v1.begin(), v2.begin()), + CombinedIterator(v1.begin() + 4, v2.begin() + 4), + CombinedIterator(v1.end(), v2.end())); + ASSERT_TRUE(std::is_sorted(v1.begin(), v1.end())); + ASSERT_TRUE(std::is_sorted(v2.begin(), v2.end())); + + AssertCollectionEq(v1, {1, 2, 3, 4, 5, 6, 7}); + AssertCollectionEq(v2, {10, 20, 30, 40, 50, 60, 70}); +} + +} // namespace android diff --git a/libs/androidfw/tests/Theme_bench.cpp b/libs/androidfw/tests/Theme_bench.cpp index dfbb5a76dec6..bf89617635cc 100644 --- a/libs/androidfw/tests/Theme_bench.cpp +++ b/libs/androidfw/tests/Theme_bench.cpp @@ -27,6 +27,10 @@ constexpr const static char* kFrameworkPath = "/system/framework/framework-res.a constexpr const static uint32_t kStyleId = 0x01030237u; // android:style/Theme.Material.Light constexpr const static uint32_t kAttrId = 0x01010030u; // android:attr/colorForeground +constexpr const static uint32_t kStyle2Id = 0x01030224u; // android:style/Theme.Material +constexpr const static uint32_t kStyle3Id = 0x0103024du; // android:style/Widget.Material +constexpr const static uint32_t kStyle4Id = 0x0103028eu; // android:style/Widget.Material.Light + static void BM_ThemeApplyStyleFramework(benchmark::State& state) { auto apk = ApkAssets::Load(kFrameworkPath); if (apk == nullptr) { @@ -61,6 +65,32 @@ static void BM_ThemeApplyStyleFrameworkOld(benchmark::State& state) { } BENCHMARK(BM_ThemeApplyStyleFrameworkOld); +static void BM_ThemeRebaseFramework(benchmark::State& state) { + auto apk = ApkAssets::Load(kFrameworkPath); + if (apk == nullptr) { + state.SkipWithError("Failed to load assets"); + return; + } + + AssetManager2 assets; + assets.SetApkAssets({apk}); + + // Create two arrays of styles to switch between back and forth. + const uint32_t styles1[] = {kStyle2Id, kStyleId, kStyle3Id}; + const uint8_t force1[std::size(styles1)] = {false, true, false}; + const uint32_t styles2[] = {kStyleId, kStyle2Id, kStyle4Id, kStyle3Id}; + const uint8_t force2[std::size(styles2)] = {false, true, true, false}; + const auto theme = assets.NewTheme(); + // Initialize the theme to make the first iteration the same as the rest. + theme->Rebase(&assets, styles1, force1, std::size(force1)); + + while (state.KeepRunning()) { + theme->Rebase(&assets, styles2, force2, std::size(force2)); + theme->Rebase(&assets, styles1, force1, std::size(force1)); + } +} +BENCHMARK(BM_ThemeRebaseFramework); + static void BM_ThemeGetAttribute(benchmark::State& state) { auto apk = ApkAssets::Load(kFrameworkPath); |