diff options
Diffstat (limited to 'libs')
85 files changed, 2979 insertions, 538 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/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml index 7d5f9cdbebc8..5fe3f2af63a0 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml @@ -14,88 +14,100 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/maximize_menu" - style="?android:attr/buttonBarStyle" android:layout_width="@dimen/desktop_mode_maximize_menu_width" android:layout_height="@dimen/desktop_mode_maximize_menu_height" - android:orientation="horizontal" - android:gravity="center" - android:padding="16dp" android:background="@drawable/desktop_mode_maximize_menu_background" android:elevation="1dp"> <LinearLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical"> + android:id="@+id/container" + android:layout_width="@dimen/desktop_mode_maximize_menu_width" + android:layout_height="@dimen/desktop_mode_maximize_menu_height" + android:orientation="horizontal" + android:padding="16dp" + android:gravity="center"> - <Button - android:layout_width="94dp" - android:layout_height="60dp" - android:id="@+id/maximize_menu_maximize_button" - style="?android:attr/buttonBarButtonStyle" - android:stateListAnimator="@null" - android:layout_marginRight="8dp" - android:layout_marginBottom="4dp" - android:alpha="0"/> - - <TextView - android:id="@+id/maximize_menu_maximize_window_text" - android:layout_width="94dp" - android:layout_height="18dp" - android:textSize="11sp" - android:layout_marginBottom="76dp" - android:gravity="center" - android:fontFamily="google-sans-text" - android:text="@string/desktop_mode_maximize_menu_maximize_text" - android:textColor="?androidprv:attr/materialColorOnSurface" - android:alpha="0"/> - </LinearLayout> - - <LinearLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical"> <LinearLayout - android:id="@+id/maximize_menu_snap_menu_layout" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:orientation="horizontal" - android:padding="4dp" - android:background="@drawable/desktop_mode_maximize_menu_layout_background" - android:layout_marginBottom="4dp" - android:alpha="0"> - <Button - android:id="@+id/maximize_menu_snap_left_button" - style="?android:attr/buttonBarButtonStyle" - android:layout_width="41dp" - android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" - android:layout_marginRight="4dp" - android:background="@drawable/desktop_mode_maximize_menu_button_background" - android:stateListAnimator="@null"/> + android:orientation="vertical"> <Button - android:id="@+id/maximize_menu_snap_right_button" + android:layout_width="94dp" + android:layout_height="60dp" + android:id="@+id/maximize_menu_maximize_button" style="?android:attr/buttonBarButtonStyle" - android:layout_width="41dp" - android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" - android:background="@drawable/desktop_mode_maximize_menu_button_background" - android:stateListAnimator="@null"/> + android:stateListAnimator="@null" + android:layout_marginRight="8dp" + android:layout_marginBottom="4dp" + android:alpha="0"/> + + <TextView + android:id="@+id/maximize_menu_maximize_window_text" + android:layout_width="94dp" + android:layout_height="18dp" + android:textSize="11sp" + android:layout_marginBottom="76dp" + android:gravity="center" + android:fontFamily="google-sans-text" + android:text="@string/desktop_mode_maximize_menu_maximize_text" + android:textColor="?androidprv:attr/materialColorOnSurface" + android:alpha="0"/> + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical"> + <LinearLayout + android:id="@+id/maximize_menu_snap_menu_layout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="4dp" + android:background="@drawable/desktop_mode_maximize_menu_layout_background" + android:layout_marginBottom="4dp" + android:alpha="0"> + <Button + android:id="@+id/maximize_menu_snap_left_button" + style="?android:attr/buttonBarButtonStyle" + android:layout_width="41dp" + android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" + android:layout_marginRight="4dp" + android:background="@drawable/desktop_mode_maximize_menu_button_background" + android:stateListAnimator="@null"/> + + <Button + android:id="@+id/maximize_menu_snap_right_button" + style="?android:attr/buttonBarButtonStyle" + android:layout_width="41dp" + android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" + android:background="@drawable/desktop_mode_maximize_menu_button_background" + android:stateListAnimator="@null"/> + </LinearLayout> + <TextView + android:id="@+id/maximize_menu_snap_window_text" + android:layout_width="94dp" + android:layout_height="18dp" + android:textSize="11sp" + android:layout_marginBottom="76dp" + android:layout_gravity="center" + android:gravity="center" + android:fontFamily="google-sans-text" + android:text="@string/desktop_mode_maximize_menu_snap_text" + android:textColor="?androidprv:attr/materialColorOnSurface" + android:alpha="0"/> </LinearLayout> - <TextView - android:id="@+id/maximize_menu_snap_window_text" - android:layout_width="94dp" - android:layout_height="18dp" - android:textSize="11sp" - android:layout_marginBottom="76dp" - android:layout_gravity="center" - android:gravity="center" - android:fontFamily="google-sans-text" - android:text="@string/desktop_mode_maximize_menu_snap_text" - android:textColor="?androidprv:attr/materialColorOnSurface" - android:alpha="0"/> </LinearLayout> -</LinearLayout> + + <!-- Empty view intentionally placed in front of everything else and matching the menu size + used to monitor input events over the entire menu. --> + <View + android:id="@+id/maximize_menu_overlay" + android:layout_width="@dimen/desktop_mode_maximize_menu_width" + android:layout_height="@dimen/desktop_mode_maximize_menu_height"/> +</FrameLayout> 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/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index 3ded7d246499..bebfa908e19a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -124,6 +124,15 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } /** + * Limited scope callback to notify when a task is removed from the system. This signal is + * not synchronized with anything (or any transition), and should not be used in cases where + * that is necessary. + */ + public interface TaskVanishedListener { + default void onTaskVanished(RunningTaskInfo taskInfo) {} + } + + /** * Callbacks for events on a task with a locus id. */ public interface LocusIdListener { @@ -167,6 +176,9 @@ public class ShellTaskOrganizer extends TaskOrganizer implements private final ArraySet<FocusListener> mFocusListeners = new ArraySet<>(); + // Listeners that should be notified when a task is removed + private final ArraySet<TaskVanishedListener> mTaskVanishedListeners = new ArraySet<>(); + private final Object mLock = new Object(); private StartingWindowController mStartingWindow; @@ -409,7 +421,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } /** - * Removes listener. + * Removes a locus id listener. */ public void removeLocusIdListener(LocusIdListener listener) { synchronized (mLock) { @@ -430,7 +442,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } /** - * Removes listener. + * Removes a focus listener. */ public void removeFocusListener(FocusListener listener) { synchronized (mLock) { @@ -439,6 +451,24 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } /** + * Adds a listener to be notified when a task vanishes. + */ + public void addTaskVanishedListener(TaskVanishedListener listener) { + synchronized (mLock) { + mTaskVanishedListeners.add(listener); + } + } + + /** + * Removes a task-vanished listener. + */ + public void removeTaskVanishedListener(TaskVanishedListener listener) { + synchronized (mLock) { + mTaskVanishedListeners.remove(listener); + } + } + + /** * Returns a surface which can be used to attach overlays to the home root task */ @NonNull @@ -614,6 +644,9 @@ public class ShellTaskOrganizer extends TaskOrganizer implements t.apply(); ProtoLog.v(WM_SHELL_TASK_ORG, "Removing overlay surface"); } + for (TaskVanishedListener l : mTaskVanishedListeners) { + l.onTaskVanished(taskInfo); + } if (!ENABLE_SHELL_TRANSITIONS && (appearedInfo.getLeash() != null)) { // Preemptively clean up the leash only if shell transitions are not enabled diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt index a3111b31a2f9..c9d3dbdcae05 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt @@ -324,6 +324,7 @@ abstract class CrossActivityBackAnimation( enteringHasSameLetterbox = false lastPostCommitFlingScale = SPRING_SCALE gestureProgress = 0f + triggerBack = false } protected fun applyTransform( @@ -499,10 +500,12 @@ abstract class CrossActivityBackAnimation( } override fun onBackCancelled() { + triggerBack = false progressAnimator.onBackCancelled { finishAnimation() } } override fun onBackInvoked() { + triggerBack = true progressAnimator.reset() onGestureCommitted(progressAnimator.velocity) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index 1279fc42c066..2aefc64a3ebb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -894,11 +894,22 @@ public class Bubble implements BubbleViewProvider { } @Nullable - Intent getAppBubbleIntent() { + @VisibleForTesting + public Intent getAppBubbleIntent() { return mAppIntent; } /** + * Sets the intent for a bubble that is an app bubble (one for which {@link #mIsAppBubble} is + * true). + * + * @param appIntent The intent to set for the app bubble. + */ + void setAppBubbleIntent(Intent appIntent) { + mAppIntent = appIntent; + } + + /** * Returns whether this bubble is from an app versus a notification. */ public boolean isAppBubble() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index d2c36e6b637c..c853301519e9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -1450,6 +1450,8 @@ public class BubbleController implements ConfigurationChangeListener, if (b != null) { // It's in the overflow, so remove it & reinflate mBubbleData.dismissBubbleWithKey(appBubbleKey, Bubbles.DISMISS_NOTIF_CANCEL); + // Update the bubble entry in the overflow with the latest intent. + b.setAppBubbleIntent(intent); } else { // App bubble does not exist, lets add and expand it b = Bubble.createAppBubble(intent, user, icon, mMainExecutor); 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 87bd84017dee..4ea41d5256f9 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,12 +17,14 @@ 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; import android.os.UserManager; import android.view.Choreographer; import android.view.IWindowManager; +import android.view.SurfaceControl; import android.view.WindowManager; import com.android.internal.jank.InteractionJankMonitor; @@ -400,7 +402,8 @@ public abstract class WMShellModule { Optional<RecentTasksController> recentTasksController, HomeTransitionObserver homeTransitionObserver) { return new RecentsTransitionHandler(shellInit, transitions, - recentTasksController.orElse(null), homeTransitionObserver); + recentTasksController.orElse(null), homeTransitionObserver, + SurfaceControl.Transaction::new); } // @@ -512,6 +515,7 @@ public abstract class WMShellModule { RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, DragAndDropController dragAndDropController, Transitions transitions, + KeyguardManager keyguardManager, EnterDesktopTaskTransitionHandler enterDesktopTransitionHandler, ExitDesktopTaskTransitionHandler exitDesktopTransitionHandler, ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler, @@ -526,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, @@ -642,6 +646,7 @@ public abstract class WMShellModule { ShellInit shellInit, ShellController shellController, ShellCommandHandler shellCommandHandler, + ShellTaskOrganizer shellTaskOrganizer, DisplayController displayController, UiEventLogger uiEventLogger, IconProvider iconProvider, @@ -649,8 +654,8 @@ public abstract class WMShellModule { Transitions transitions, @ShellMainThread ShellExecutor mainExecutor) { return new DragAndDropController(context, shellInit, shellController, shellCommandHandler, - displayController, uiEventLogger, iconProvider, globalDragListener, transitions, - mainExecutor); + shellTaskOrganizer, displayController, uiEventLogger, iconProvider, + globalDragListener, transitions, mainExecutor); } // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java index 677fd5deffd3..240cf3b96e89 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java @@ -212,12 +212,13 @@ public abstract class Pip1Module { @WMSingleton @Provides static PipMotionHelper providePipMotionHelper(Context context, + @ShellMainThread ShellExecutor mainExecutor, PipBoundsState pipBoundsState, PipTaskOrganizer pipTaskOrganizer, PhonePipMenuController menuController, PipSnapAlgorithm pipSnapAlgorithm, PipTransitionController pipTransitionController, FloatingContentCoordinator floatingContentCoordinator, Optional<PipPerfHintController> pipPerfHintControllerOptional) { - return new PipMotionHelper(context, pipBoundsState, pipTaskOrganizer, + return new PipMotionHelper(context, mainExecutor, pipBoundsState, pipTaskOrganizer, menuController, pipSnapAlgorithm, pipTransitionController, floatingContentCoordinator, pipPerfHintControllerOptional); } 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 14ae3a7b9b27..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,10 +974,16 @@ 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, - "DesktopTasksController: switch freeform task to fullscreen oon transition" + + "DesktopTasksController: bring desktop tasks to front on transition" + " taskId=%d", task.taskId ) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt index 88d0554669b7..5335c0b69a24 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt @@ -27,6 +27,8 @@ import android.window.TransitionInfo import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction import androidx.core.animation.addListener +import com.android.internal.jank.Cuj +import com.android.wm.shell.common.InteractionJankMonitorUtils import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener @@ -103,6 +105,8 @@ class ToggleResizeDesktopTaskTransitionHandler( onTaskResizeAnimationListener.onAnimationEnd(taskId) finishCallback.onTransitionFinished(null) boundsAnimator = null + InteractionJankMonitorUtils.endTracing( + Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW) } ) addUpdateListener { anim -> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md index 438aa768165e..b1cbe8d98397 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md @@ -73,7 +73,7 @@ stack traces when specific surface transaction calls are made, which is possible following system properties for example: ```shell # Enabling -adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha # matches the name of the SurfaceControlTransaction method +adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha,setPosition # matches the name of the SurfaceControlTransaction methods adb shell setprop persist.wm.debug.sc.tx.log_match_name com.android.systemui # matches the name of the surface adb reboot adb logcat -s "SurfaceControlRegistry" @@ -87,6 +87,16 @@ adb reboot It is not necessary to set both `log_match_call` and `log_match_name`, but note logs can be quite noisy if unfiltered. +It can sometimes be useful to trace specific logs and when they are applied (sometimes we build +transactions that can be applied later). You can do this by adding the "merge" and "apply" calls to +the set of requested calls: +```shell +# Enabling +adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha,merge,apply # apply will dump logs of each setAlpha or merge call on that tx +adb reboot +adb logcat -s "SurfaceControlRegistry" +``` + ## Tracing activity starts in the app process It's sometimes useful to know when to see a stack trace of when an activity starts in the app code diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java index c374eb8e8f03..a4813a3ebfd8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java @@ -62,6 +62,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.protolog.common.ProtoLog; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; @@ -85,6 +86,7 @@ import java.util.function.Function; public class DragAndDropController implements RemoteCallable<DragAndDropController>, GlobalDragListener.GlobalDragListenerCallback, DisplayController.OnDisplaysChangedListener, + ShellTaskOrganizer.TaskVanishedListener, View.OnDragListener, ComponentCallbacks2 { private static final String TAG = DragAndDropController.class.getSimpleName(); @@ -92,6 +94,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll private final Context mContext; private final ShellController mShellController; private final ShellCommandHandler mShellCommandHandler; + private final ShellTaskOrganizer mShellTaskOrganizer; private final DisplayController mDisplayController; private final DragAndDropEventLogger mLogger; private final IconProvider mIconProvider; @@ -133,6 +136,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll ShellInit shellInit, ShellController shellController, ShellCommandHandler shellCommandHandler, + ShellTaskOrganizer shellTaskOrganizer, DisplayController displayController, UiEventLogger uiEventLogger, IconProvider iconProvider, @@ -142,6 +146,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll mContext = context; mShellController = shellController; mShellCommandHandler = shellCommandHandler; + mShellTaskOrganizer = shellTaskOrganizer; mDisplayController = displayController; mLogger = new DragAndDropEventLogger(uiEventLogger); mIconProvider = iconProvider; @@ -163,6 +168,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll }, 0); mShellController.addExternalInterface(KEY_EXTRA_SHELL_DRAG_AND_DROP, this::createExternalInterface, this); + mShellTaskOrganizer.addTaskVanishedListener(this); mShellCommandHandler.addDumpCallback(this::dump, this); mGlobalDragListener.setListener(this); } @@ -281,6 +287,34 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll } @Override + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + if (taskInfo.baseIntent == null) { + // Invalid info + return; + } + // Find the active drag + PerDisplay pd = null; + for (int i = 0; i < mDisplayDropTargets.size(); i++) { + final PerDisplay iPd = mDisplayDropTargets.valueAt(i); + if (iPd.isHandlingDrag) { + pd = iPd; + break; + } + } + if (pd == null || !pd.isHandlingDrag) { + // Not currently dragging + return; + } + + // Update the drag session + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Handling vanished task: id=%d component=%s", taskInfo.taskId, + taskInfo.baseIntent.getComponent()); + pd.dragSession.updateRunningTask(); + pd.dragLayout.updateSession(pd.dragSession); + } + + @Override public boolean onDrag(View target, DragEvent event) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Drag event: action=%s x=%f y=%f xOffset=%f yOffset=%f", @@ -313,11 +347,10 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll Slog.w(TAG, "Unexpected drag start during an active drag"); return false; } - // TODO(b/290391688): Also update the session data with task stack changes pd.dragSession = new DragSession(ActivityTaskManager.getInstance(), mDisplayController.getDisplayLayout(displayId), event.getClipData(), event.getDragFlags()); - pd.dragSession.update(); + pd.dragSession.initialize(); pd.activeDragCount++; pd.dragLayout.prepare(pd.dragSession, mLogger.logStart(pd.dragSession)); setDropTargetWindowVisibility(pd, View.VISIBLE); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java index a42ca1905ee7..b1882fcae242 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java @@ -84,7 +84,10 @@ public class DragAndDropPolicy { private static final String TAG = DragAndDropPolicy.class.getSimpleName(); private final Context mContext; - private final Starter mStarter; + // Used only for launching a fullscreen task (or as a fallback if there is no split starter) + private final Starter mFullscreenStarter; + // Used for launching tasks into splitscreen + private final Starter mSplitscreenStarter; private final SplitScreenController mSplitScreen; private final ArrayList<DragAndDropPolicy.Target> mTargets = new ArrayList<>(); private final RectF mDisallowHitRegion = new RectF(); @@ -97,10 +100,12 @@ public class DragAndDropPolicy { } @VisibleForTesting - DragAndDropPolicy(Context context, SplitScreenController splitScreen, Starter starter) { + DragAndDropPolicy(Context context, SplitScreenController splitScreen, + Starter fullscreenStarter) { mContext = context; mSplitScreen = splitScreen; - mStarter = mSplitScreen != null ? mSplitScreen : starter; + mFullscreenStarter = fullscreenStarter; + mSplitscreenStarter = splitScreen; } /** @@ -245,17 +250,20 @@ public class DragAndDropPolicy { mSplitScreen.onDroppedToSplit(position, mLoggerSessionId); } + final Starter starter = target.type == TYPE_FULLSCREEN + ? mFullscreenStarter + : mSplitscreenStarter; if (mSession.appData != null) { - launchApp(mSession, position); + launchApp(mSession, starter, position); } else { - launchIntent(mSession, position); + launchIntent(mSession, starter, position); } } /** * Launches an app provided by SysUI. */ - private void launchApp(DragSession session, @SplitPosition int position) { + private void launchApp(DragSession session, Starter starter, @SplitPosition int position) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Launching app data at position=%d", position); final ClipDescription description = session.getClipDescription(); @@ -275,11 +283,11 @@ public class DragAndDropPolicy { if (isTask) { final int taskId = session.appData.getIntExtra(EXTRA_TASK_ID, INVALID_TASK_ID); - mStarter.startTask(taskId, position, opts); + starter.startTask(taskId, position, opts); } else if (isShortcut) { final String packageName = session.appData.getStringExtra(EXTRA_PACKAGE_NAME); final String id = session.appData.getStringExtra(EXTRA_SHORTCUT_ID); - mStarter.startShortcut(packageName, id, position, opts, user); + starter.startShortcut(packageName, id, position, opts, user); } else { final PendingIntent launchIntent = session.appData.getParcelableExtra(EXTRA_PENDING_INTENT); @@ -288,7 +296,7 @@ public class DragAndDropPolicy { Log.e(TAG, "Expected app intent's EXTRA_USER to match pending intent user"); } } - mStarter.startIntent(launchIntent, user.getIdentifier(), null /* fillIntent */, + starter.startIntent(launchIntent, user.getIdentifier(), null /* fillIntent */, position, opts); } } @@ -296,7 +304,7 @@ public class DragAndDropPolicy { /** * Launches an intent sender provided by an application. */ - private void launchIntent(DragSession session, @SplitPosition int position) { + private void launchIntent(DragSession session, Starter starter, @SplitPosition int position) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Launching intent at position=%d", position); final ActivityOptions baseActivityOpts = ActivityOptions.makeBasic(); @@ -309,7 +317,7 @@ public class DragAndDropPolicy { | FLAG_ACTIVITY_MULTIPLE_TASK); final Bundle opts = baseActivityOpts.toBundle(); - mStarter.startIntent(session.launchableIntent, + starter.startIntent(session.launchableIntent, session.launchableIntent.getCreatorUserHandle().getIdentifier(), null /* fillIntent */, position, opts); } @@ -420,7 +428,7 @@ public class DragAndDropPolicy { @Override public String toString() { - return "Target {hit=" + hitRegion + " draw=" + drawRegion + "}"; + return "Target {type=" + type + " hit=" + hitRegion + " draw=" + drawRegion + "}"; } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java index 4bb10dfdf8c6..5df83be8622b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java @@ -42,6 +42,7 @@ import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Insets; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.Drawable; @@ -102,6 +103,8 @@ public class DragLayout extends LinearLayout private boolean mIsShowing; private boolean mHasDropped; private DragSession mSession; + // The last position that was handled by the drag layout + private final Point mLastPosition = new Point(); @SuppressLint("WrongConstant") public DragLayout(Context context, SplitScreenController splitScreenController, @@ -265,6 +268,15 @@ public class DragLayout extends LinearLayout */ public void prepare(DragSession session, InstanceId loggerSessionId) { mPolicy.start(session, loggerSessionId); + updateSession(session); + } + + /** + * Updates the drag layout based on the diven drag session. + */ + public void updateSession(DragSession session) { + // Note: The policy currently just keeps a reference to the session + boolean updatingExistingSession = mSession != null; mSession = session; mHasDropped = false; mCurrentTarget = null; @@ -312,6 +324,11 @@ public class DragLayout extends LinearLayout updateDropZoneSizes(topOrLeftBounds, bottomOrRightBounds); } requestLayout(); + if (updatingExistingSession) { + // Update targets if we are already currently dragging + recomputeDropTargets(); + update(mLastPosition.x, mLastPosition.y); + } } private void updateDropZoneSizesForSingleTask() { @@ -359,6 +376,9 @@ public class DragLayout extends LinearLayout mDropZoneView2.setLayoutParams(dropZoneView2); } + /** + * Shows the drag layout. + */ public void show() { mIsShowing = true; recomputeDropTargets(); @@ -384,13 +404,19 @@ public class DragLayout extends LinearLayout * Updates the visible drop target as the user drags. */ public void update(DragEvent event) { + update((int) event.getX(), (int) event.getY()); + } + + /** + * Updates the visible drop target as the user drags to the given coordinates. + */ + private void update(int x, int y) { if (mHasDropped) { return; } // Find containing region, if the same as mCurrentRegion, then skip, otherwise, animate the // visibility of the current region - DragAndDropPolicy.Target target = mPolicy.getTargetAtLocation( - (int) event.getX(), (int) event.getY()); + DragAndDropPolicy.Target target = mPolicy.getTargetAtLocation(x, y); if (mCurrentTarget != target) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Current target: %s", target); if (target == null) { @@ -429,6 +455,7 @@ public class DragLayout extends LinearLayout } mCurrentTarget = target; } + mLastPosition.set(x, y); } /** @@ -436,6 +463,7 @@ public class DragLayout extends LinearLayout */ public void hide(DragEvent event, Runnable hideCompleteCallback) { mIsShowing = false; + mLastPosition.set(-1, -1); animateSplitContainers(false, () -> { if (hideCompleteCallback != null) { hideCompleteCallback.run(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java index 0addd432aff0..41a50b1c8e8f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java @@ -30,7 +30,9 @@ import android.content.pm.ActivityInfo; import androidx.annotation.Nullable; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.util.List; @@ -79,17 +81,27 @@ public class DragSession { } /** - * Updates the session data based on the current state of the system. + * Updates the running task for this drag session. */ - void update() { - List<ActivityManager.RunningTaskInfo> tasks = + void updateRunningTask() { + final List<ActivityManager.RunningTaskInfo> tasks = mActivityTaskManager.getTasks(1, false /* filterOnlyVisibleRecents */); if (!tasks.isEmpty()) { final ActivityManager.RunningTaskInfo task = tasks.get(0); runningTaskInfo = task; runningTaskWinMode = task.getWindowingMode(); runningTaskActType = task.getActivityType(); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Running task: id=%d component=%s", task.taskId, + task.baseIntent != null ? task.baseIntent.getComponent() : "null"); } + } + + /** + * Updates the session data based on the current state of the system at the start of the drag. + */ + void initialize() { + updateRunningTask(); activityInfo = mInitialDragData.getItemAt(0).getActivityInfo(); // TODO: This should technically check & respect config_supportsNonResizableMultiWindow diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java index 724a130ef52d..2ccadb81935d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java @@ -19,6 +19,7 @@ package com.android.wm.shell.draganddrop; import static com.android.wm.shell.animation.Interpolators.FAST_OUT_SLOW_IN; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Canvas; @@ -37,13 +38,16 @@ import android.widget.ImageView; import androidx.annotation.Nullable; import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; +import com.android.wm.shell.protolog.ShellProtoLogGroup; /** * Renders a drop zone area for items being dragged. */ public class DropZoneView extends FrameLayout { + private static final boolean DEBUG_LAYOUT = false; private static final float SPLASHSCREEN_ALPHA = 0.90f; private static final float HIGHLIGHT_ALPHA = 1f; private static final int MARGIN_ANIMATION_ENTER_DURATION = 400; @@ -77,6 +81,7 @@ public class DropZoneView extends FrameLayout { private int mHighlightColor; private ObjectAnimator mBackgroundAnimator; + private int mTargetBackgroundColor; private ObjectAnimator mMarginAnimator; private float mMarginPercent; @@ -181,6 +186,9 @@ public class DropZoneView extends FrameLayout { /** Animates between highlight and splashscreen depending on current state. */ public void animateSwitch() { + if (DEBUG_LAYOUT) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "animateSwitch"); + } mShowingHighlight = !mShowingHighlight; mShowingSplash = !mShowingHighlight; final int newColor = mShowingHighlight ? mHighlightColor : mSplashScreenColor; @@ -190,6 +198,10 @@ public class DropZoneView extends FrameLayout { /** Animates the highlight indicating the zone is hovered on or not. */ public void setShowingHighlight(boolean showingHighlight) { + if (DEBUG_LAYOUT) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "setShowingHighlight: showing=%b", + showingHighlight); + } mShowingHighlight = showingHighlight; mShowingSplash = !mShowingHighlight; final int newColor = mShowingHighlight ? mHighlightColor : mSplashScreenColor; @@ -199,6 +211,10 @@ public class DropZoneView extends FrameLayout { /** Animates the margins around the drop zone to show or hide. */ public void setShowingMargin(boolean visible) { + if (DEBUG_LAYOUT) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "setShowingMargin: visible=%b", + visible); + } if (mShowingMargin != visible) { mShowingMargin = visible; animateMarginToState(); @@ -212,6 +228,15 @@ public class DropZoneView extends FrameLayout { } private void animateBackground(int startColor, int endColor) { + if (DEBUG_LAYOUT) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "animateBackground: start=%s end=%s", + Integer.toHexString(startColor), Integer.toHexString(endColor)); + } + if (endColor == mTargetBackgroundColor) { + // Already at, or animating to, that background color + return; + } if (mBackgroundAnimator != null) { mBackgroundAnimator.cancel(); } @@ -223,6 +248,7 @@ public class DropZoneView extends FrameLayout { mBackgroundAnimator.setInterpolator(FAST_OUT_SLOW_IN); } mBackgroundAnimator.start(); + mTargetBackgroundColor = endColor; } private void animateSplashScreenIcon() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java index ce98458c0575..93ede7a8b7aa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java @@ -16,7 +16,6 @@ package com.android.wm.shell.pip; -import android.content.ComponentName; import android.os.RemoteException; import android.view.IPinnedTaskListener; import android.view.WindowManagerGlobal; @@ -70,12 +69,6 @@ public class PinnedStackListenerForwarder { } } - private void onActivityHidden(ComponentName componentName) { - for (PinnedTaskListener listener : mListeners) { - listener.onActivityHidden(componentName); - } - } - @BinderThread private class PinnedTaskListenerImpl extends IPinnedTaskListener.Stub { @Override @@ -91,13 +84,6 @@ public class PinnedStackListenerForwarder { PinnedStackListenerForwarder.this.onImeVisibilityChanged(imeVisible, imeHeight); }); } - - @Override - public void onActivityHidden(ComponentName componentName) { - mMainExecutor.execute(() -> { - PinnedStackListenerForwarder.this.onActivityHidden(componentName); - }); - } } /** @@ -108,7 +94,5 @@ public class PinnedStackListenerForwarder { public void onMovementBoundsChanged(boolean fromImeAdjustment) {} public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {} - - public void onActivityHidden(ComponentName componentName) {} } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java index a749019046f8..b27c428f1693 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java @@ -16,10 +16,12 @@ package com.android.wm.shell.pip; +import android.annotation.NonNull; import android.graphics.Rect; import com.android.wm.shell.shared.annotations.ExternalThread; +import java.util.concurrent.Executor; import java.util.function.Consumer; /** @@ -69,9 +71,10 @@ public interface Pip { default void removePipExclusionBoundsChangeListener(Consumer<Rect> listener) { } /** - * @return {@link PipTransitionController} instance. + * Register {@link PipTransitionController.PipTransitionCallback} to listen on PiP transition + * started / finished callbacks. */ - default PipTransitionController getPipTransitionController() { - return null; - } + default void registerPipTransitionCallback( + @NonNull PipTransitionController.PipTransitionCallback callback, + @NonNull Executor executor) { } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index e2e1ecde8b56..3fae37014fba 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -423,7 +423,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, }); mPipTransitionController.setPipOrganizer(this); displayController.addDisplayWindowListener(this); - pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback); + pipTransitionController.registerPipTransitionCallback( + mPipTransitionCallback, mMainExecutor); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 3cae72d89ecc..f3a8fbf85754 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -24,6 +24,7 @@ import static android.util.RotationUtils.rotateBounds; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_TO_BACK; @@ -300,6 +301,10 @@ public class PipTransition extends PipTransitionController { finishTransaction); } + if (isCurrentPipActivityClosed(info)) { + mPipBoundsState.setLastPipComponentName(null /* componentName */); + } + return false; } @@ -322,6 +327,21 @@ public class PipTransition extends PipTransitionController { return true; } + private boolean isCurrentPipActivityClosed(TransitionInfo info) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + TransitionInfo.Change change = info.getChanges().get(i); + boolean isTaskChange = change.getTaskInfo() != null; + boolean hasComponentNameOfPip = change.getActivityComponent() != null + && change.getActivityComponent().equals( + mPipBoundsState.getLastPipComponentName()); + if (!isTaskChange && change.getMode() == TRANSIT_CLOSE && hasComponentNameOfPip) { + return true; + } + } + return false; + } + + @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index 6eefdcfc4d93..a7c47f92eb14 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -53,8 +53,9 @@ import com.android.wm.shell.transition.DefaultMixedHandler; import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Executor; /** * Responsible supplying PiP Transitions. @@ -66,7 +67,7 @@ public abstract class PipTransitionController implements Transitions.TransitionH protected final ShellTaskOrganizer mShellTaskOrganizer; protected final PipMenuController mPipMenuController; protected final Transitions mTransitions; - private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>(); + private final Map<PipTransitionCallback, Executor> mPipTransitionCallbacks = new HashMap<>(); protected PipTaskOrganizer mPipOrganizer; protected DefaultMixedHandler mMixedHandler; @@ -181,16 +182,18 @@ public abstract class PipTransitionController implements Transitions.TransitionH /** * Registers {@link PipTransitionCallback} to receive transition callbacks. */ - public void registerPipTransitionCallback(PipTransitionCallback callback) { - mPipTransitionCallbacks.add(callback); + public void registerPipTransitionCallback( + @NonNull PipTransitionCallback callback, @NonNull Executor executor) { + mPipTransitionCallbacks.put(callback, executor); } protected void sendOnPipTransitionStarted( @PipAnimationController.TransitionDirection int direction) { final Rect pipBounds = mPipBoundsState.getBounds(); - for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { - final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); - callback.onPipTransitionStarted(direction, pipBounds); + for (Map.Entry<PipTransitionCallback, Executor> entry + : mPipTransitionCallbacks.entrySet()) { + entry.getValue().execute( + () -> entry.getKey().onPipTransitionStarted(direction, pipBounds)); } if (isInPipDirection(direction) && Flags.enablePipUiStateCallbackOnEntering()) { try { @@ -207,9 +210,10 @@ public abstract class PipTransitionController implements Transitions.TransitionH protected void sendOnPipTransitionFinished( @PipAnimationController.TransitionDirection int direction) { - for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { - final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); - callback.onPipTransitionFinished(direction); + for (Map.Entry<PipTransitionCallback, Executor> entry + : mPipTransitionCallbacks.entrySet()) { + entry.getValue().execute( + () -> entry.getKey().onPipTransitionFinished(direction)); } if (isInPipDirection(direction) && Flags.enablePipUiStateCallbackOnEntering()) { try { @@ -226,9 +230,10 @@ public abstract class PipTransitionController implements Transitions.TransitionH protected void sendOnPipTransitionCancelled( @PipAnimationController.TransitionDirection int direction) { - for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { - final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); - callback.onPipTransitionCanceled(direction); + for (Map.Entry<PipTransitionCallback, Executor> entry + : mPipTransitionCallbacks.entrySet()) { + entry.getValue().execute( + () -> entry.getKey().onPipTransitionCanceled(direction)); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 8c4bf7620068..448d4f527d16 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -106,6 +106,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.Executor; import java.util.function.Consumer; /** @@ -367,15 +368,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb false /* fromRotation */, fromImeAdjustment, false /* fromShelfAdjustment */, null /* windowContainerTransaction */); } - - @Override - public void onActivityHidden(ComponentName componentName) { - if (componentName.equals(mPipBoundsState.getLastPipComponentName())) { - // The activity was removed, we don't want to restore to the reentry state - // saved for this component anymore. - mPipBoundsState.setLastPipComponentName(null); - } - } } /** @@ -487,7 +479,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb mShellCommandHandler.addDumpCallback(this::dump, this); mPipInputConsumer = new PipInputConsumer(WindowManagerGlobal.getWindowManagerService(), INPUT_CONSUMER_PIP, mMainExecutor); - mPipTransitionController.registerPipTransitionCallback(this); + mPipTransitionController.registerPipTransitionCallback(this, mMainExecutor); mPipTaskOrganizer.registerOnDisplayIdChangeCallback((int displayId) -> { mPipDisplayLayoutState.setDisplayId(displayId); onDisplayChanged(mDisplayController.getDisplayLayout(displayId), @@ -1229,8 +1221,11 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public PipTransitionController getPipTransitionController() { - return mPipTransitionController; + public void registerPipTransitionCallback( + PipTransitionController.PipTransitionCallback callback, + Executor executor) { + mMainExecutor.execute(() -> mPipTransitionController.registerPipTransitionCallback( + callback, executor)); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java index ef468434db6a..f5bd006b4621 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java @@ -38,6 +38,7 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.animation.FloatProperties; import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.magnetictarget.MagnetizedObject; import com.android.wm.shell.common.pip.PipAppOpsListener; import com.android.wm.shell.common.pip.PipBoundsState; @@ -47,6 +48,7 @@ import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.animation.PhysicsAnimator; +import com.android.wm.shell.shared.annotations.ShellMainThread; import kotlin.Unit; import kotlin.jvm.functions.Function0; @@ -171,7 +173,9 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, public void onPipTransitionCanceled(int direction) {} }; - public PipMotionHelper(Context context, @NonNull PipBoundsState pipBoundsState, + public PipMotionHelper(Context context, + @ShellMainThread ShellExecutor mainExecutor, + @NonNull PipBoundsState pipBoundsState, PipTaskOrganizer pipTaskOrganizer, PhonePipMenuController menuController, PipSnapAlgorithm snapAlgorithm, PipTransitionController pipTransitionController, FloatingContentCoordinator floatingContentCoordinator, @@ -183,7 +187,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, mSnapAlgorithm = snapAlgorithm; mFloatingContentCoordinator = floatingContentCoordinator; mPipPerfHintController = pipPerfHintControllerOptional.orElse(null); - pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback); + pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback, mainExecutor); mResizePipUpdateListener = (target, values) -> { if (mPipBoundsState.getMotionBoundsState().isInMotion()) { mPipTaskOrganizer.scheduleUserResizePip(getBounds(), diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java index 3d286461ef79..b6a7c56527bd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java @@ -257,7 +257,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } private void onInit() { - mPipTransitionController.registerPipTransitionCallback(this); + mPipTransitionController.registerPipTransitionCallback(this, mMainExecutor); reloadResources(); 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/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index 3a266d9bb3ef..c67cf1d85918 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -74,6 +74,7 @@ import com.android.wm.shell.transition.Transitions; import java.util.ArrayList; import java.util.function.Consumer; +import java.util.function.Supplier; /** * Handles the Recents (overview) animation. Only one of these can run at a time. A recents @@ -84,6 +85,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { private final Transitions mTransitions; private final ShellExecutor mExecutor; + private final Supplier<SurfaceControl.Transaction> mTransactionSupplier; @Nullable private final RecentTasksController mRecentTasksController; private IApplicationThread mAnimApp = null; @@ -101,11 +103,13 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { public RecentsTransitionHandler(ShellInit shellInit, Transitions transitions, @Nullable RecentTasksController recentTasksController, - HomeTransitionObserver homeTransitionObserver) { + HomeTransitionObserver homeTransitionObserver, + Supplier<SurfaceControl.Transaction> transactionSupplier) { mTransitions = transitions; mExecutor = transitions.getMainExecutor(); mRecentTasksController = recentTasksController; mHomeTransitionObserver = homeTransitionObserver; + mTransactionSupplier = transactionSupplier; if (!Transitions.ENABLE_SHELL_TRANSITIONS) return; if (recentTasksController == null) return; shellInit.addInitCallback(() -> { @@ -1056,7 +1060,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { final Transitions.TransitionFinishCallback finishCB = mFinishCB; mFinishCB = null; - final SurfaceControl.Transaction t = mFinishTransaction; + SurfaceControl.Transaction t = mFinishTransaction; final WindowContainerTransaction wct = new WindowContainerTransaction(); if (mKeyguardLocked && mRecentsTask != null) { @@ -1106,6 +1110,16 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " normal finish"); + if (toHome && !mOpeningTasks.isEmpty()) { + // Attempting to start a task after swipe to home, don't show it, + // move recents to top + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + " attempting to start a task after swipe to home"); + t = mTransactionSupplier.get(); + wct.reorder(mRecentsTask, true /*onTop*/); + mClosingTasks.addAll(mOpeningTasks); + mOpeningTasks.clear(); + } // The general case: committing to recents, going home, or switching tasks. for (int i = 0; i < mOpeningTasks.size(); ++i) { t.show(mOpeningTasks.get(i).mTaskSurface); @@ -1174,6 +1188,10 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { mPipTransaction = null; } } + if (t != mFinishTransaction) { + // apply after merges because these changes are accounting for finishWCT changes. + mTransitions.setAfterMergeFinishTransaction(mTransition, t); + } cleanUp(); finishCB.onTransitionFinished(wct.isEmpty() ? null : wct); if (runnerFinishCb != null) { 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/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index b6a18e537600..45eff4a24898 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -2649,7 +2649,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Nullable TransitionRequestInfo request) { final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask(); if (triggerTask == null) { - if (isSplitActive()) { + if (isSplitScreenVisible()) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d display rotation", request.getDebugId()); // Check if the display is rotating. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java index b03daaafd70c..35427b93acea 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java @@ -94,6 +94,11 @@ public class CounterRotatorHelper { return rotatedBounds; } + /** Returns true if the change is put on a surface in previous rotation. */ + public boolean isRotated(@NonNull TransitionInfo.Change change) { + return mLastRotationDelta != 0 && mRotatorMap.containsKey(change.getParent()); + } + /** * Removes the counter rotation surface in the finish transaction. No need to reparent the * children as the finish transaction should have already taken care of that. 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 018c9044e2f7..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; @@ -517,7 +518,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { animRelOffset.y = Math.max(animRelOffset.y, change.getEndRelOffset().y); } - if (change.getActivityComponent() != null && !isActivityLevel) { + if (change.getActivityComponent() != null && !isActivityLevel + && !mRotator.isRotated(change)) { // At this point, this is an independent activity change in a non-activity // transition. This means that an activity transition got erroneously combined // with another ongoing transition. This then means that the animation root may @@ -943,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())) { @@ -964,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 f257e207673d..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; @@ -238,6 +241,13 @@ public class Transitions implements RemoteCallable<Transitions>, /** Ordered list of transitions which have been merged into this one. */ private ArrayList<ActiveTransition> mMerged; + /** + * @deprecated DO NOT USE THIS unless absolutely necessary. It will be removed once + * everything migrates off finishWCT. + */ + @java.lang.Deprecated + SurfaceControl.Transaction mAfterMergeFinishT; + ActiveTransition(IBinder token) { mToken = token; } @@ -512,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(); @@ -1018,6 +1033,20 @@ public class Transitions implements RemoteCallable<Transitions>, return null; } + /** @deprecated */ + @java.lang.Deprecated + public void setAfterMergeFinishTransaction(IBinder transition, + SurfaceControl.Transaction afterMergeFinishT) { + final ActiveTransition at = mKnownTransitions.get(transition); + if (at == null) return; + if (at.mAfterMergeFinishT != null) { + Log.e(TAG, "Setting after-merge-t >1 time on transition: " + at.mInfo.getDebugId()); + at.mAfterMergeFinishT.merge(afterMergeFinishT); + return; + } + at.mAfterMergeFinishT = afterMergeFinishT; + } + /** Aborts a transition. This will still queue it up to maintain order. */ private void onAbort(ActiveTransition transition) { final Track track = mTracks.get(transition.getTrack()); @@ -1078,6 +1107,7 @@ public class Transitions implements RemoteCallable<Transitions>, } // Merge all associated transactions together SurfaceControl.Transaction fullFinish = active.mFinishT; + SurfaceControl.Transaction afterMergeFinish = active.mAfterMergeFinishT; if (active.mMerged != null) { for (int iM = 0; iM < active.mMerged.size(); ++iM) { final ActiveTransition toMerge = active.mMerged.get(iM); @@ -1097,6 +1127,21 @@ public class Transitions implements RemoteCallable<Transitions>, fullFinish.merge(toMerge.mFinishT); } } + if (toMerge.mAfterMergeFinishT != null) { + if (afterMergeFinish == null) { + afterMergeFinish = toMerge.mAfterMergeFinishT; + } else { + afterMergeFinish.merge(toMerge.mAfterMergeFinishT); + } + toMerge.mAfterMergeFinishT = null; + } + } + } + if (afterMergeFinish != null) { + if (fullFinish == null) { + fullFinish = afterMergeFinish; + } else { + fullFinish.merge(afterMergeFinish); } } if (fullFinish != null) { 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/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 5e7e5e6bc460..180e4f999726 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -26,7 +26,6 @@ import static android.view.InputDevice.SOURCE_TOUCHSCREEN; import static android.view.MotionEvent.ACTION_CANCEL; import static android.view.MotionEvent.ACTION_HOVER_ENTER; import static android.view.MotionEvent.ACTION_HOVER_EXIT; -import static android.view.MotionEvent.ACTION_HOVER_MOVE; import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_UP; import static android.view.WindowInsets.Type.statusBars; @@ -73,6 +72,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.jank.Cuj; import com.android.internal.protolog.common.ProtoLog; import com.android.window.flags.Flags; import com.android.wm.shell.R; @@ -81,6 +81,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.InteractionJankMonitorUtils; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; @@ -101,6 +102,7 @@ import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.ExclusionRegionListener; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; +import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; import java.io.PrintWriter; import java.util.Objects; @@ -381,10 +383,32 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mWindowDecorByTaskId.remove(taskInfo.taskId); } + private void onMaximizeOrRestore(int taskId, String tag) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + if (decoration == null) { + return; + } + InteractionJankMonitorUtils.beginTracing( + Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, mContext, decoration.mTaskSurface, tag); + mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo); + decoration.closeHandleMenu(); + decoration.closeMaximizeMenu(); + } + + private void onSnapResize(int taskId, boolean left) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + if (decoration == null) { + return; + } + mDesktopTasksController.snapToHalfScreen(decoration.mTaskInfo, + left ? SnapPosition.LEFT : SnapPosition.RIGHT); + decoration.closeHandleMenu(); + decoration.closeMaximizeMenu(); + } + private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener, View.OnGenericMotionListener, DragDetector.MotionEventHandler { - private static final int CLOSE_MAXIMIZE_MENU_DELAY_MS = 150; private final int mTaskId; private final WindowContainerToken mTaskToken; @@ -403,7 +427,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private boolean mTouchscreenInUse; private boolean mHasLongClicked; private int mDragPointerId = -1; - private final Runnable mCloseMaximizeWindowRunnable; private DesktopModeTouchEventListener( RunningTaskInfo taskInfo, @@ -414,11 +437,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDragDetector = new DragDetector(this); mGestureDetector = new GestureDetector(mContext, this); mDisplayId = taskInfo.displayId; - mCloseMaximizeWindowRunnable = () -> { - final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); - if (decoration == null) return; - decoration.closeMaximizeMenu(); - }; } @Override @@ -470,25 +488,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } else if (id == R.id.collapse_menu_button) { decoration.closeHandleMenu(); } else if (id == R.id.maximize_window) { - final RunningTaskInfo taskInfo = decoration.mTaskInfo; - decoration.closeHandleMenu(); - decoration.closeMaximizeMenu(); - mDesktopTasksController.toggleDesktopTaskSize(taskInfo); - } else if (id == R.id.maximize_menu_maximize_button) { - final RunningTaskInfo taskInfo = decoration.mTaskInfo; - mDesktopTasksController.toggleDesktopTaskSize(taskInfo); - decoration.closeHandleMenu(); - decoration.closeMaximizeMenu(); - } else if (id == R.id.maximize_menu_snap_left_button) { - final RunningTaskInfo taskInfo = decoration.mTaskInfo; - mDesktopTasksController.snapToHalfScreen(taskInfo, SnapPosition.LEFT); - decoration.closeHandleMenu(); - decoration.closeMaximizeMenu(); - } else if (id == R.id.maximize_menu_snap_right_button) { - final RunningTaskInfo taskInfo = decoration.mTaskInfo; - mDesktopTasksController.snapToHalfScreen(taskInfo, SnapPosition.RIGHT); - decoration.closeHandleMenu(); - decoration.closeMaximizeMenu(); + // TODO(b/346441962): move click detection logic into the decor's + // {@link AppHeaderViewHolder}. Let it encapsulate the that and have it report + // back to the decoration using + // {@link DesktopModeWindowDecoration#setOnMaximizeOrRestoreClickListener}, which + // should shared with the maximize menu's maximize/restore actions. + onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button"); } } @@ -570,40 +575,26 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { return false; } + /** + * TODO(b/346441962): move this hover detection logic into the decor's + * {@link AppHeaderViewHolder}. + */ @Override public boolean onGenericMotion(View v, MotionEvent ev) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); final int id = v.getId(); - if (ev.getAction() == ACTION_HOVER_ENTER) { - if (!decoration.isMaximizeMenuActive() && id == R.id.maximize_window) { - decoration.onMaximizeWindowHoverEnter(); - } else if (id == R.id.maximize_window - || MaximizeMenu.Companion.isMaximizeMenuView(id)) { - // Re-hovering over any of the maximize menu views should keep the menu open by - // cancelling any attempts to close the menu. - mMainHandler.removeCallbacks(mCloseMaximizeWindowRunnable); - if (id != R.id.maximize_window) { - decoration.onMaximizeMenuHoverEnter(id, ev); - } + if (ev.getAction() == ACTION_HOVER_ENTER && id == R.id.maximize_window) { + decoration.setAppHeaderMaximizeButtonHovered(true); + if (!decoration.isMaximizeMenuActive()) { + decoration.onMaximizeButtonHoverEnter(); } return true; - } else if (ev.getAction() == ACTION_HOVER_MOVE - && MaximizeMenu.Companion.isMaximizeMenuView(id)) { - decoration.onMaximizeMenuHoverMove(id, ev); - mMainHandler.removeCallbacks(mCloseMaximizeWindowRunnable); - } else if (ev.getAction() == ACTION_HOVER_EXIT) { - if (!decoration.isMaximizeMenuActive() && id == R.id.maximize_window) { - decoration.onMaximizeWindowHoverExit(); - } else if (id == R.id.maximize_window - || MaximizeMenu.Companion.isMaximizeMenuView(id)) { - // Close menu if not hovering over maximize menu or maximize button after a - // delay to give user a chance to re-enter view or to move from one maximize - // menu view to another. - mMainHandler.postDelayed(mCloseMaximizeWindowRunnable, - CLOSE_MAXIMIZE_MENU_DELAY_MS); - if (id != R.id.maximize_window) { - decoration.onMaximizeMenuHoverExit(id, ev); - } + } + if (ev.getAction() == ACTION_HOVER_EXIT && id == R.id.maximize_window) { + decoration.setAppHeaderMaximizeButtonHovered(false); + decoration.onMaximizeHoverStateChanged(); + if (!decoration.isMaximizeMenuActive()) { + decoration.onMaximizeButtonHoverExit(); } return true; } @@ -711,8 +702,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { && action != MotionEvent.ACTION_CANCEL)) { return false; } - final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); - mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo); + onMaximizeOrRestore(mTaskId, "double_tap"); return true; } } @@ -1094,7 +1084,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { final DesktopModeTouchEventListener touchEventListener = new DesktopModeTouchEventListener(taskInfo, dragPositioningCallback); - + windowDecoration.setOnMaximizeOrRestoreClickListener(this::onMaximizeOrRestore); + windowDecoration.setOnLeftSnapClickListener((taskId, tag) -> { + onSnapResize(taskId, true /* isLeft */); + }); + windowDecoration.setOnRightSnapClickListener((taskId, tag) -> { + onSnapResize(taskId, false /* isLeft */); + }); windowDecoration.setCaptionListeners( touchEventListener, touchEventListener, touchEventListener, touchEventListener); windowDecoration.setExclusionRegionListener(mExclusionRegionListener); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 4d597cac889e..f53c21d352b3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -69,6 +69,7 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.shared.DesktopModeStatus; import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; import com.android.wm.shell.windowdecor.viewholder.AppHandleViewHolder; import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; @@ -87,6 +88,9 @@ import java.util.function.Supplier; public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> { private static final String TAG = "DesktopModeWindowDecoration"; + @VisibleForTesting + static final long CLOSE_MAXIMIZE_MENU_DELAY_MS = 150L; + private final Handler mHandler; private final Choreographer mChoreographer; private final SyncTransactionQueue mSyncQueue; @@ -96,6 +100,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private View.OnTouchListener mOnCaptionTouchListener; private View.OnLongClickListener mOnCaptionLongClickListener; private View.OnGenericMotionListener mOnCaptionGenericMotionListener; + private OnTaskActionClickListener mOnMaximizeOrRestoreClickListener; + private OnTaskActionClickListener mOnLeftSnapClickListener; + private OnTaskActionClickListener mOnRightSnapClickListener; private DragPositioningCallback mDragPositioningCallback; private DragResizeInputListener mDragResizeListener; private DragDetector mDragDetector; @@ -120,6 +127,16 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private ExclusionRegionListener mExclusionRegionListener; private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; + private final MaximizeMenuFactory mMaximizeMenuFactory; + + // Hover state for the maximize menu and button. The menu will remain open as long as either of + // these is true. See {@link #onMaximizeHoverStateChanged()}. + private boolean mIsAppHeaderMaximizeButtonHovered = false; + private boolean mIsMaximizeMenuHovered = false; + // Used to schedule the closing of the maximize menu when neither of the button or menu are + // being hovered. There's a small delay after stopping the hover, to allow a quick reentry + // to cancel the close. + private final Runnable mCloseMaximizeWindowRunnable = this::closeMaximizeMenu; DesktopModeWindowDecoration( Context context, @@ -135,7 +152,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin handler, choreographer, syncQueue, rootTaskDisplayAreaOrganizer, SurfaceControl.Builder::new, SurfaceControl.Transaction::new, WindowContainerTransaction::new, SurfaceControl::new, - new SurfaceControlViewHostFactory() {}); + new SurfaceControlViewHostFactory() {}, + DefaultMaximizeMenuFactory.INSTANCE); } DesktopModeWindowDecoration( @@ -152,7 +170,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, Supplier<WindowContainerTransaction> windowContainerTransactionSupplier, Supplier<SurfaceControl> surfaceControlSupplier, - SurfaceControlViewHostFactory surfaceControlViewHostFactory) { + SurfaceControlViewHostFactory surfaceControlViewHostFactory, + MaximizeMenuFactory maximizeMenuFactory) { super(context, displayController, taskOrganizer, taskInfo, taskSurface, surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, windowContainerTransactionSupplier, surfaceControlSupplier, @@ -161,6 +180,31 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mChoreographer = choreographer; mSyncQueue = syncQueue; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; + mMaximizeMenuFactory = maximizeMenuFactory; + } + + /** + * Register a listener to be called back when one of the tasks' maximize/restore action is + * triggered. + * TODO(b/346441962): hook this up to double-tap and the header's maximize button, instead of + * having the ViewModel deal with parsing motion events. + */ + void setOnMaximizeOrRestoreClickListener(OnTaskActionClickListener listener) { + mOnMaximizeOrRestoreClickListener = listener; + } + + /** + * Register a listener to be called back when one of the tasks snap-left action is triggered. + */ + void setOnLeftSnapClickListener(OnTaskActionClickListener listener) { + mOnLeftSnapClickListener = listener; + } + + /** + * Register a listener to be called back when one of the tasks' snap-right action is triggered. + */ + void setOnRightSnapClickListener(OnTaskActionClickListener listener) { + mOnRightSnapClickListener = listener; } void setCaptionListeners( @@ -714,11 +758,41 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * Create and display maximize menu window */ void createMaximizeMenu() { - mMaximizeMenu = new MaximizeMenu(mSyncQueue, mRootTaskDisplayAreaOrganizer, - mDisplayController, mTaskInfo, mOnCaptionButtonClickListener, - mOnCaptionGenericMotionListener, mOnCaptionTouchListener, mContext, + mMaximizeMenu = mMaximizeMenuFactory.create(mSyncQueue, mRootTaskDisplayAreaOrganizer, + mDisplayController, mTaskInfo, mContext, calculateMaximizeMenuPosition(), mSurfaceControlTransactionSupplier); - mMaximizeMenu.show(); + mMaximizeMenu.show( + mOnMaximizeOrRestoreClickListener, + mOnLeftSnapClickListener, + mOnRightSnapClickListener, + hovered -> { + mIsMaximizeMenuHovered = hovered; + onMaximizeHoverStateChanged(); + return null; + } + ); + } + + /** Set whether the app header's maximize button is hovered. */ + void setAppHeaderMaximizeButtonHovered(boolean hovered) { + mIsAppHeaderMaximizeButtonHovered = hovered; + onMaximizeHoverStateChanged(); + } + + /** + * Called when either one of the maximize button in the app header or the maximize menu has + * changed its hover state. + */ + void onMaximizeHoverStateChanged() { + if (!mIsMaximizeMenuHovered && !mIsAppHeaderMaximizeButtonHovered) { + // Neither is hovered, close the menu. + if (isMaximizeMenuActive()) { + mHandler.postDelayed(mCloseMaximizeWindowRunnable, CLOSE_MAXIMIZE_MENU_DELAY_MS); + } + return; + } + // At least one of the two is hovered, cancel the close if needed. + mHandler.removeCallbacks(mCloseMaximizeWindowRunnable); } /** @@ -992,34 +1066,22 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin .setAnimatingTaskResize(animatingTaskResize); } - /** Called when there is a {@Link ACTION_HOVER_EXIT} on the maximize window button. */ - void onMaximizeWindowHoverExit() { + /** + * Called when there is a {@link MotionEvent#ACTION_HOVER_EXIT} on the maximize window button. + */ + void onMaximizeButtonHoverExit() { ((AppHeaderViewHolder) mWindowDecorViewHolder) .onMaximizeWindowHoverExit(); } - /** Called when there is a {@Link ACTION_HOVER_ENTER} on the maximize window button. */ - void onMaximizeWindowHoverEnter() { + /** + * Called when there is a {@link MotionEvent#ACTION_HOVER_ENTER} on the maximize window button. + */ + void onMaximizeButtonHoverEnter() { ((AppHeaderViewHolder) mWindowDecorViewHolder) .onMaximizeWindowHoverEnter(); } - /** Called when there is a {@Link ACTION_HOVER_ENTER} on a view in the maximize menu. */ - void onMaximizeMenuHoverEnter(int id, MotionEvent ev) { - mMaximizeMenu.onMaximizeMenuHoverEnter(id, ev); - } - - /** Called when there is a {@Link ACTION_HOVER_MOVE} on a view in the maximize menu. */ - void onMaximizeMenuHoverMove(int id, MotionEvent ev) { - mMaximizeMenu.onMaximizeMenuHoverMove(id, ev); - } - - /** Called when there is a {@Link ACTION_HOVER_EXIT} on a view in the maximize menu. */ - void onMaximizeMenuHoverExit(int id, MotionEvent ev) { - mMaximizeMenu.onMaximizeMenuHoverExit(id, ev); - } - - @Override public String toString() { return "{" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java index fe1c9c3cce66..d48ce536f2b3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java @@ -28,6 +28,8 @@ import android.graphics.Rect; import android.util.DisplayMetrics; import android.view.SurfaceControl; +import androidx.annotation.NonNull; + import com.android.window.flags.Flags; import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayController; @@ -106,13 +108,15 @@ public class DragPositioningCallbackUtility { repositionTaskBounds.bottom = (candidateBottom < stableBounds.bottom) ? candidateBottom : oldBottom; } - // If width or height are negative or less than the minimum width or height, revert the + // If width or height are negative or exceeding the width or height constraints, revert the // respective bounds to use previous bound dimensions. - if (repositionTaskBounds.width() < getMinWidth(displayController, windowDecoration)) { + if (isExceedingWidthConstraint(repositionTaskBounds, stableBounds, displayController, + windowDecoration)) { repositionTaskBounds.right = oldRight; repositionTaskBounds.left = oldLeft; } - if (repositionTaskBounds.height() < getMinHeight(displayController, windowDecoration)) { + if (isExceedingHeightConstraint(repositionTaskBounds, stableBounds, displayController, + windowDecoration)) { repositionTaskBounds.top = oldTop; repositionTaskBounds.bottom = oldBottom; } @@ -174,6 +178,30 @@ public class DragPositioningCallbackUtility { return result; } + private static boolean isExceedingWidthConstraint(@NonNull Rect repositionTaskBounds, + Rect maxResizeBounds, DisplayController displayController, + WindowDecoration windowDecoration) { + // Check if width is less than the minimum width constraint. + if (repositionTaskBounds.width() < getMinWidth(displayController, windowDecoration)) { + return true; + } + // Check if width is more than the maximum resize bounds on desktop windowing mode. + return isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext) + && repositionTaskBounds.width() > maxResizeBounds.width(); + } + + private static boolean isExceedingHeightConstraint(@NonNull Rect repositionTaskBounds, + Rect maxResizeBounds, DisplayController displayController, + WindowDecoration windowDecoration) { + // Check if height is less than the minimum height constraint. + if (repositionTaskBounds.height() < getMinHeight(displayController, windowDecoration)) { + return true; + } + // Check if height is more than the maximum resize bounds on desktop windowing mode. + return isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext) + && repositionTaskBounds.height() > maxResizeBounds.height(); + } + private static float getMinWidth(DisplayController displayController, WindowDecoration windowDecoration) { return windowDecoration.mTaskInfo.minWidth < 0 ? getDefaultMinWidth(displayController, @@ -210,7 +238,7 @@ public class DragPositioningCallbackUtility { private static float getDefaultMinSize(DisplayController displayController, WindowDecoration windowDecoration) { - float density = displayController.getDisplayLayout(windowDecoration.mTaskInfo.displayId) + float density = displayController.getDisplayLayout(windowDecoration.mTaskInfo.displayId) .densityDpi() * DisplayMetrics.DENSITY_DEFAULT_SCALE; return windowDecoration.mTaskInfo.defaultMinSize * density; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt index 0470367015ea..5f9f8d6d1764 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt @@ -20,7 +20,6 @@ import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.animation.ValueAnimator import android.annotation.ColorInt -import android.annotation.IdRes import android.app.ActivityManager.RunningTaskInfo import android.content.Context import android.content.res.ColorStateList @@ -28,6 +27,7 @@ import android.content.res.Resources import android.graphics.Paint import android.graphics.PixelFormat import android.graphics.PointF +import android.graphics.Rect import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import android.graphics.drawable.LayerDrawable @@ -37,16 +37,17 @@ import android.graphics.drawable.shapes.RoundRectShape import android.util.StateSet import android.view.LayoutInflater import android.view.MotionEvent +import android.view.MotionEvent.ACTION_HOVER_ENTER +import android.view.MotionEvent.ACTION_HOVER_EXIT +import android.view.MotionEvent.ACTION_HOVER_MOVE import android.view.SurfaceControl import android.view.SurfaceControl.Transaction import android.view.SurfaceControlViewHost import android.view.View -import android.view.View.OnClickListener -import android.view.View.OnGenericMotionListener -import android.view.View.OnTouchListener import android.view.View.SCALE_Y import android.view.View.TRANSLATION_Y import android.view.View.TRANSLATION_Z +import android.view.ViewGroup import android.view.WindowManager import android.view.WindowlessWindowManager import android.widget.Button @@ -64,10 +65,10 @@ import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHo import com.android.wm.shell.windowdecor.common.DecorThemeUtil import com.android.wm.shell.windowdecor.common.OPACITY_12 import com.android.wm.shell.windowdecor.common.OPACITY_40 +import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener import com.android.wm.shell.windowdecor.common.withAlpha import java.util.function.Supplier - /** * Menu that appears when user long clicks the maximize button. Gives the user the option to * maximize the task or snap the task to the right or left half of the screen. @@ -77,9 +78,6 @@ class MaximizeMenu( private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer, private val displayController: DisplayController, private val taskInfo: RunningTaskInfo, - private val onClickListener: OnClickListener, - private val onGenericMotionListener: OnGenericMotionListener, - private val onTouchListener: OnTouchListener, private val decorWindowContext: Context, private val menuPosition: PointF, private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() } @@ -102,9 +100,19 @@ class MaximizeMenu( } /** Creates and shows the maximize window. */ - fun show() { + fun show( + onMaximizeClickListener: OnTaskActionClickListener, + onLeftSnapClickListener: OnTaskActionClickListener, + onRightSnapClickListener: OnTaskActionClickListener, + onHoverListener: (Boolean) -> Unit + ) { if (maximizeMenu != null) return - createMaximizeMenu() + createMaximizeMenu( + onMaximizeClickListener = onMaximizeClickListener, + onLeftSnapClickListener = onLeftSnapClickListener, + onRightSnapClickListener = onRightSnapClickListener, + onHoverListener = onHoverListener + ) maximizeMenuView?.animateOpenMenu() } @@ -117,7 +125,12 @@ class MaximizeMenu( } /** Create a maximize menu that is attached to the display area. */ - private fun createMaximizeMenu() { + private fun createMaximizeMenu( + onMaximizeClickListener: OnTaskActionClickListener, + onLeftSnapClickListener: OnTaskActionClickListener, + onRightSnapClickListener: OnTaskActionClickListener, + onHoverListener: (Boolean) -> Unit + ) { val t = transactionSupplier.get() val builder = SurfaceControl.Builder() rootTdaOrganizer.attachToDisplayArea(taskInfo.displayId, builder) @@ -146,11 +159,19 @@ class MaximizeMenu( context = decorWindowContext, menuHeight = menuHeight, menuPadding = menuPadding, - onClickListener = onClickListener, - onTouchListener = onTouchListener, - onGenericMotionListener = onGenericMotionListener, ).also { menuView -> + val taskId = taskInfo.taskId menuView.bind(taskInfo) + menuView.onMaximizeClickListener = { + onMaximizeClickListener.onClick(taskId, "maximize_menu_option") + } + menuView.onLeftSnapClickListener = { + onLeftSnapClickListener.onClick(taskId, "left_snap_option") + } + menuView.onRightSnapClickListener = { + onRightSnapClickListener.onClick(taskId, "right_snap_option") + } + menuView.onMenuHoverListener = onHoverListener viewHost.setView(menuView.rootView, lp) } @@ -198,56 +219,6 @@ class MaximizeMenu( } /** - * Called when a [MotionEvent.ACTION_HOVER_ENTER] is triggered on any of the menu's views. - * - * TODO(b/346440693): this is only needed for the left/right snap options that don't support - * selector states to manage its hover state. Look into whether that can be added to avoid - * manually tracking hover enter/exit motion events. Also because those button colors/states - * aren't updating correctly for pressed, focused and selected states. - * See also [onMaximizeMenuHoverMove] and [onMaximizeMenuHoverExit]. - */ - fun onMaximizeMenuHoverEnter(viewId: Int, ev: MotionEvent) { - setSnapButtonsColorOnHover(viewId, ev) - } - - /** Called when a [MotionEvent.ACTION_HOVER_MOVE] is triggered on any of the menu's views. */ - fun onMaximizeMenuHoverMove(viewId: Int, ev: MotionEvent) { - setSnapButtonsColorOnHover(viewId, ev) - } - - /** Called when a [MotionEvent.ACTION_HOVER_EXIT] is triggered on any of the menu's views. */ - fun onMaximizeMenuHoverExit(id: Int, ev: MotionEvent) { - val snapOptionsWidth = maximizeMenuView?.snapOptionsWidth ?: return - val snapOptionsHeight = maximizeMenuView?.snapOptionsHeight ?: return - val inSnapMenuBounds = ev.x >= 0 && ev.x <= snapOptionsWidth && - ev.y >= 0 && ev.y <= snapOptionsHeight - - if (id == R.id.maximize_menu_snap_menu_layout && !inSnapMenuBounds) { - // After exiting the snap menu layout area, checks to see that user is not still - // hovering within the snap menu layout bounds which would indicate that the user is - // hovering over a snap button within the snap menu layout rather than having exited. - maximizeMenuView?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.NONE) - } - } - - private fun setSnapButtonsColorOnHover(viewId: Int, ev: MotionEvent) { - val snapOptionsWidth = maximizeMenuView?.snapOptionsWidth ?: return - val snapMenuCenter = snapOptionsWidth / 2 - when { - viewId == R.id.maximize_menu_snap_left_button || - (viewId == R.id.maximize_menu_snap_menu_layout && ev.x <= snapMenuCenter) -> { - maximizeMenuView - ?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.LEFT) - } - viewId == R.id.maximize_menu_snap_right_button || - (viewId == R.id.maximize_menu_snap_menu_layout && ev.x > snapMenuCenter) -> { - maximizeMenuView - ?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.RIGHT) - } - } - } - - /** * The view within the Maximize Menu, presents maximize, restore and snap-to-side options for * resizing a Task. */ @@ -255,12 +226,11 @@ class MaximizeMenu( context: Context, private val menuHeight: Int, private val menuPadding: Int, - onClickListener: OnClickListener, - onTouchListener: OnTouchListener, - onGenericMotionListener: OnGenericMotionListener, ) { - val rootView: View = LayoutInflater.from(context) - .inflate(R.layout.desktop_mode_window_decor_maximize_menu, null /* root */) + val rootView = LayoutInflater.from(context) + .inflate(R.layout.desktop_mode_window_decor_maximize_menu, null /* root */) as ViewGroup + private val container = requireViewById(R.id.container) + private val overlay = requireViewById(R.id.maximize_menu_overlay) private val maximizeText = requireViewById(R.id.maximize_menu_maximize_window_text) as TextView private val maximizeButton = @@ -285,30 +255,63 @@ class MaximizeMenu( private val fillRadius = context.resources .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_radius) + private val hoverTempRect = Rect() private val openMenuAnimatorSet = AnimatorSet() private lateinit var taskInfo: RunningTaskInfo private lateinit var style: MenuStyle - /** The width of the snap menu option view, including both left and right snaps. */ - val snapOptionsWidth: Int - get() = snapButtonsLayout.width - /** The height of the snap menu option view, including both left and right snaps .*/ - val snapOptionsHeight: Int - get() = snapButtonsLayout.height + /** Invoked when the maximize or restore option is clicked. */ + var onMaximizeClickListener: (() -> Unit)? = null + /** Invoked when the left snap option is clicked. */ + var onLeftSnapClickListener: (() -> Unit)? = null + /** Invoked when the right snap option is clicked. */ + var onRightSnapClickListener: (() -> Unit)? = null + /** Invoked whenever the hover state of the menu changes. */ + var onMenuHoverListener: ((Boolean) -> Unit)? = null init { - // TODO(b/346441962): encapsulate menu hover enter/exit logic inside this class and - // expose only what is actually relevant to outside classes so that specific checks - // against resource IDs aren't needed outside this class. - rootView.setOnGenericMotionListener(onGenericMotionListener) - rootView.setOnTouchListener(onTouchListener) - maximizeButton.setOnClickListener(onClickListener) - maximizeButton.setOnGenericMotionListener(onGenericMotionListener) - snapRightButton.setOnClickListener(onClickListener) - snapRightButton.setOnGenericMotionListener(onGenericMotionListener) - snapLeftButton.setOnClickListener(onClickListener) - snapLeftButton.setOnGenericMotionListener(onGenericMotionListener) - snapButtonsLayout.setOnGenericMotionListener(onGenericMotionListener) + overlay.setOnHoverListener { _, event -> + // The overlay covers the entire menu, so it's a convenient way to monitor whether + // the menu is hovered as a whole or not. + when (event.action) { + ACTION_HOVER_ENTER -> onMenuHoverListener?.invoke(true) + ACTION_HOVER_EXIT -> onMenuHoverListener?.invoke(false) + } + + // Also check if the hover falls within the snap options layout, to manually + // set the left/right state based on the event's position. + // TODO(b/346440693): this manual hover tracking is needed for left/right snap + // because its view/background(s) don't support selector states. Look into whether + // that can be added to avoid manual tracking. Also because these button + // colors/state logic is only being applied on hover events, but there's pressed, + // focused and selected states that should be responsive too. + val snapLayoutBoundsRelToOverlay = hoverTempRect.also { rect -> + snapButtonsLayout.getDrawingRect(rect) + rootView.offsetDescendantRectToMyCoords(snapButtonsLayout, rect) + } + if (event.action == ACTION_HOVER_ENTER || event.action == ACTION_HOVER_MOVE) { + if (snapLayoutBoundsRelToOverlay.contains(event.x.toInt(), event.y.toInt())) { + // Hover is inside the snap layout, anything left of center is the left + // snap, and anything right of center is right snap. + val layoutCenter = snapLayoutBoundsRelToOverlay.centerX() + if (event.x < layoutCenter) { + updateSplitSnapSelection(SnapToHalfSelection.LEFT) + } else { + updateSplitSnapSelection(SnapToHalfSelection.RIGHT) + } + } else { + // Any other hover is outside the snap layout, so neither is selected. + updateSplitSnapSelection(SnapToHalfSelection.NONE) + } + } + + // Don't consume the event to allow child views to receive the event too. + return@setOnHoverListener false + } + + maximizeButton.setOnClickListener { onMaximizeClickListener?.invoke() } + snapRightButton.setOnClickListener { onRightSnapClickListener?.invoke() } + snapLeftButton.setOnClickListener { onLeftSnapClickListener?.invoke() } // To prevent aliasing. maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) @@ -351,7 +354,7 @@ class MaximizeMenu( val value = animatedValue as Float val topPadding = menuPadding - ((1 - value) * menuHeight).toInt() - rootView.setPadding(menuPadding, topPadding, + container.setPadding(menuPadding, topPadding, menuPadding, menuPadding) } }, @@ -410,7 +413,7 @@ class MaximizeMenu( } /** Update the view state to a new snap to half selection. */ - fun updateSplitSnapSelection(selection: SnapToHalfSelection) { + private fun updateSplitSnapSelection(selection: SnapToHalfSelection) { when (selection) { SnapToHalfSelection.NONE -> deactivateSnapOptions() SnapToHalfSelection.LEFT -> activateSnapOption(activateLeft = true) @@ -638,13 +641,41 @@ class MaximizeMenu( private const val ELEVATION_ANIMATION_DURATION_MS = 50L private const val CONTROLS_ALPHA_ANIMATION_DELAY_MS = 33L private const val MENU_Z_TRANSLATION = 1f - fun isMaximizeMenuView(@IdRes viewId: Int): Boolean { - return viewId == R.id.maximize_menu || - viewId == R.id.maximize_menu_maximize_button || - viewId == R.id.maximize_menu_snap_left_button || - viewId == R.id.maximize_menu_snap_right_button || - viewId == R.id.maximize_menu_snap_menu_layout || - viewId == R.id.maximize_menu_snap_menu_layout - } + } +} + +/** A factory interface to create a [MaximizeMenu]. */ +interface MaximizeMenuFactory { + fun create( + syncQueue: SyncTransactionQueue, + rootTdaOrganizer: RootTaskDisplayAreaOrganizer, + displayController: DisplayController, + taskInfo: RunningTaskInfo, + decorWindowContext: Context, + menuPosition: PointF, + transactionSupplier: Supplier<Transaction> + ): MaximizeMenu +} + +/** A [MaximizeMenuFactory] implementation that creates a [MaximizeMenu]. */ +object DefaultMaximizeMenuFactory : MaximizeMenuFactory { + override fun create( + syncQueue: SyncTransactionQueue, + rootTdaOrganizer: RootTaskDisplayAreaOrganizer, + displayController: DisplayController, + taskInfo: RunningTaskInfo, + decorWindowContext: Context, + menuPosition: PointF, + transactionSupplier: Supplier<Transaction> + ): MaximizeMenu { + return MaximizeMenu( + syncQueue, + rootTdaOrganizer, + displayController, + taskInfo, + decorWindowContext, + menuPosition, + transactionSupplier + ) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/OnTaskActionClickListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/OnTaskActionClickListener.kt new file mode 100644 index 000000000000..14b9e7f71622 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/OnTaskActionClickListener.kt @@ -0,0 +1,27 @@ +/* + * 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.windowdecor.common + +/** A callback to be invoked when a Task's window decor element is clicked. */ +fun interface OnTaskActionClickListener { + /** + * Called when a task's decor element has been clicked. + * + * @param taskId the id of the task. + * @param tag a readable identifier for the element. + */ + fun onClick(taskId: Int, tag: String) +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java index f9b4108bc8c2..8303317d39fc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java @@ -687,6 +687,25 @@ public class ShellTaskOrganizerTests extends ShellTestCase { verify(mRecentTasksController).onTaskRunningInfoChanged(task2); } + @Test + public void testTaskVanishedCallback() { + RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN); + mOrganizer.onTaskAppeared(task1, /* leash= */ null); + + RunningTaskInfo[] vanishedTasks = new RunningTaskInfo[1]; + ShellTaskOrganizer.TaskVanishedListener listener = + new ShellTaskOrganizer.TaskVanishedListener() { + @Override + public void onTaskVanished(RunningTaskInfo taskInfo) { + vanishedTasks[0] = taskInfo; + } + }; + mOrganizer.addTaskVanishedListener(listener); + mOrganizer.onTaskVanished(task1); + + assertEquals(vanishedTasks[0], task1); + } + private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) { RunningTaskInfo taskInfo = new RunningTaskInfo(); taskInfo.taskId = taskId; 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 a1a18a9b7c9d..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, @@ -439,14 +442,15 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun showDesktopApps_dontReorderMinimizedTask() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_desktopWallpaperDisabled_dontReorderMinimizedTask() { val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() val minimizedTask = setUpFreeformTask() + markTaskHidden(freeformTask) markTaskHidden(minimizedTask) desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId) - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) @@ -457,6 +461,26 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_desktopWallpaperEnabled_dontReorderMinimizedTask() { + setUpHomeTask() + val freeformTask = setUpFreeformTask() + val minimizedTask = setUpFreeformTask() + + markTaskHidden(freeformTask) + markTaskHidden(minimizedTask) + desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId) + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(2) + // Add desktop wallpaper activity + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + // Reorder freeform task to top, don't reorder the minimized task + wct.assertReorderAt(index = 1, freeformTask, toTop = true) + } + + @Test fun getVisibleTaskCount_noTasks_returnsZero() { assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) } @@ -645,16 +669,33 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun moveToDesktop_nonRunningTask_launchesInFreeform() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveToDesktop_desktopWallpaperDisabled_nonRunningTask_launchesInFreeform() { + val task = createTaskInfo(1) whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) + whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) - val task = createTaskInfo(1) + controller.moveToDesktop(task.taskId, transitionSource = UNKNOWN) + + with(getLatestEnterDesktopWct()) { + assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM) + } + } + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveToDesktop_desktopWallpaperEnabled_nonRunningTask_launchesInFreeform() { + val task = createTaskInfo(1) + whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) controller.moveToDesktop(task.taskId, transitionSource = UNKNOWN) + with(getLatestEnterDesktopWct()) { - assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM) + // Add desktop wallpaper activity + assertPendingIntentAt(index = 0, desktopWallpaperIntent) + // Launch task + assertLaunchTaskAt(index = 1, task.taskId, WINDOWING_MODE_FREEFORM) } } @@ -776,21 +817,44 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun moveToDesktop_bringsTasksOverLimit_dontShowBackTask() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveToDesktop_desktopWallpaperDisabled_bringsTasksOver_dontShowBackTask() { val taskLimit = desktopTasksLimiter.getMaxTaskLimit() - val homeTask = setUpHomeTask() val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } val newTask = setUpFullscreenTask() + val homeTask = setUpHomeTask() controller.moveToDesktop(newTask, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() assertThat(wct.hierarchyOps.size).isEqualTo(taskLimit + 1) // visible tasks + home wct.assertReorderAt(0, homeTask) - for (i in 1..<taskLimit) { // Skipping freeformTasks[0] - wct.assertReorderAt(index = i, task = freeformTasks[i]) - } - wct.assertReorderAt(taskLimit, newTask) + wct.assertReorderSequenceInRange( + range = 1..<(taskLimit + 1), + *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0] + newTask + ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveToDesktop_desktopWallpaperEnabled_bringsTasksOverLimit_dontShowBackTask() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } + val newTask = setUpFullscreenTask() + setUpHomeTask() + + controller.moveToDesktop(newTask, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + assertThat(wct.hierarchyOps.size).isEqualTo(taskLimit + 1) // visible tasks + wallpaper + // Add desktop wallpaper activity + wct.assertPendingIntentAt(0, desktopWallpaperIntent) + wct.assertReorderSequenceInRange( + range = 1..<(taskLimit + 1), + *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0] + newTask + ) } @Test @@ -1109,41 +1173,106 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun handleRequest_freeformTask_freeformNotVisible_reorderedToTop() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() { assumeTrue(ENABLE_SHELL_TRANSITIONS) val freeformTask1 = setUpFreeformTask() + val freeformTask2 = createFreeformTask() + markTaskHidden(freeformTask1) + val result = + controller.handleRequest(Binder(), createTransition(freeformTask2, type = TRANSIT_TO_FRONT)) + + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(2) + result.assertReorderAt(1, freeformTask2, toTop = true) + } + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val freeformTask1 = setUpFreeformTask() val freeformTask2 = createFreeformTask() + + markTaskHidden(freeformTask1) val result = - controller.handleRequest(Binder(), createTransition(freeformTask2, type = TRANSIT_TO_FRONT)) + controller.handleRequest(Binder(), createTransition(freeformTask2, type = TRANSIT_TO_FRONT)) - assertThat(result?.hierarchyOps?.size).isEqualTo(2) - result!!.assertReorderAt(1, freeformTask2, toTop = true) + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(3) + // Add desktop wallpaper activity + result.assertPendingIntentAt(0, desktopWallpaperIntent) + // Bring active desktop tasks to front + result.assertReorderAt(1, freeformTask1, toTop = true) + // Bring new task to front + result.assertReorderAt(2, freeformTask2, toTop = true) } @Test - fun handleRequest_freeformTask_noOtherTasks_reorderedToTop() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_desktopWallpaperDisabled_noOtherTasks_reorderedToTop() { assumeTrue(ENABLE_SHELL_TRANSITIONS) val task = createFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task)) - assertThat(result?.hierarchyOps?.size).isEqualTo(1) - result!!.assertReorderAt(0, task, toTop = true) + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(1) + result.assertReorderAt(0, task, toTop = true) } @Test - fun handleRequest_freeformTask_freeformOnOtherDisplayOnly_reorderedToTop() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val task = createFreeformTask() + val result = controller.handleRequest(Binder(), createTransition(task)) + + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(2) + // Add desktop wallpaper activity + result.assertPendingIntentAt(0, desktopWallpaperIntent) + // Bring new task to front + result.assertReorderAt(1, task, toTop = true) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_dskWallpaperDisabled_freeformOnOtherDisplayOnly_reorderedToTop() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) + // Second display task + createFreeformTask(displayId = SECOND_DISPLAY) + + val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay)) + + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(1) + result.assertReorderAt(0, taskDefaultDisplay, toTop = true) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() { assumeTrue(ENABLE_SHELL_TRANSITIONS) val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) - val taskSecondDisplay = createFreeformTask(displayId = SECOND_DISPLAY) + // Second display task + createFreeformTask(displayId = SECOND_DISPLAY) val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay)) - assertThat(result?.hierarchyOps?.size).isEqualTo(1) - result!!.assertReorderAt(0, taskDefaultDisplay, toTop = true) + + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(2) + // Add desktop wallpaper activity + result.assertPendingIntentAt(0, desktopWallpaperIntent) + // Bring new task to front + result.assertReorderAt(1, taskDefaultDisplay, toTop = true) } @Test @@ -1175,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) @@ -2041,6 +2181,16 @@ private fun WindowContainerTransaction.assertReorderSequence(vararg tasks: Runni } } +/** Checks if the reorder hierarchy operations in [range] correspond to [tasks] list */ +private fun WindowContainerTransaction.assertReorderSequenceInRange( + range: IntRange, + vararg tasks: RunningTaskInfo +) { + assertThat(hierarchyOps.slice(range).map { it.type to it.container }) + .containsExactlyElementsIn(tasks.map { HIERARCHY_OP_TYPE_REORDER to it.token.asBinder() }) + .inOrder() +} + private fun WindowContainerTransaction.assertRemoveAt(index: Int, token: WindowContainerToken) { assertIndexInBounds(index) val op = hierarchyOps[index] diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java index a64ebd301c00..840126421c08 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java @@ -76,6 +76,8 @@ public class DragAndDropControllerTest extends ShellTestCase { @Mock private ShellCommandHandler mShellCommandHandler; @Mock + private ShellTaskOrganizer mShellTaskOrganizer; + @Mock private DisplayController mDisplayController; @Mock private UiEventLogger mUiEventLogger; @@ -96,8 +98,8 @@ public class DragAndDropControllerTest extends ShellTestCase { public void setUp() throws RemoteException { MockitoAnnotations.initMocks(this); mController = new DragAndDropController(mContext, mShellInit, mShellController, - mShellCommandHandler, mDisplayController, mUiEventLogger, mIconProvider, - mGlobalDragListener, mTransitions, mMainExecutor); + mShellCommandHandler, mShellTaskOrganizer, mDisplayController, mUiEventLogger, + mIconProvider, mGlobalDragListener, mTransitions, mMainExecutor); mController.onInit(); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java index 6e72e8df8d62..582fb91559e5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java @@ -65,8 +65,6 @@ import android.content.res.Resources; import android.graphics.Insets; import android.os.RemoteException; import android.view.DisplayInfo; -import android.view.DragEvent; -import android.view.View; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -76,7 +74,6 @@ import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.draganddrop.DragAndDropPolicy.Target; import com.android.wm.shell.splitscreen.SplitScreenController; -import com.android.wm.shell.startingsurface.TaskSnapshotWindow; import org.junit.After; import org.junit.Before; @@ -106,6 +103,8 @@ public class DragAndDropPolicyTest extends ShellTestCase { // Both the split-screen and start interface. @Mock private SplitScreenController mSplitScreenStarter; + @Mock + private DragAndDropPolicy.Starter mFullscreenStarter; @Mock private InstanceId mLoggerSessionId; @@ -151,7 +150,7 @@ public class DragAndDropPolicyTest extends ShellTestCase { mPortraitDisplayLayout = new DisplayLayout(info2, res, false, false); mInsets = Insets.of(0, 0, 0, 0); - mPolicy = spy(new DragAndDropPolicy(mContext, mSplitScreenStarter, mSplitScreenStarter)); + mPolicy = spy(new DragAndDropPolicy(mContext, mSplitScreenStarter, mFullscreenStarter)); mActivityClipData = createAppClipData(MIMETYPE_APPLICATION_ACTIVITY); mLaunchableIntentPendingIntent = mock(PendingIntent.class); when(mLaunchableIntentPendingIntent.getCreatorUserHandle()) @@ -285,13 +284,13 @@ public class DragAndDropPolicyTest extends ShellTestCase { setRunningTask(mHomeTask); DragSession dragSession = new DragSession(mActivityTaskManager, mLandscapeDisplayLayout, data, 0 /* dragFlags */); - dragSession.update(); + dragSession.initialize(); mPolicy.start(dragSession, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( mPolicy.getTargets(mInsets), TYPE_FULLSCREEN); mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN)); - verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), + verify(mFullscreenStarter).startIntent(any(), anyInt(), any(), eq(SPLIT_POSITION_UNDEFINED), any()); } @@ -300,7 +299,7 @@ public class DragAndDropPolicyTest extends ShellTestCase { setRunningTask(mFullscreenAppTask); DragSession dragSession = new DragSession(mActivityTaskManager, mLandscapeDisplayLayout, data, 0 /* dragFlags */); - dragSession.update(); + dragSession.initialize(); mPolicy.start(dragSession, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( mPolicy.getTargets(mInsets), TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); @@ -320,7 +319,7 @@ public class DragAndDropPolicyTest extends ShellTestCase { setRunningTask(mFullscreenAppTask); DragSession dragSession = new DragSession(mActivityTaskManager, mPortraitDisplayLayout, data, 0 /* dragFlags */); - dragSession.update(); + dragSession.initialize(); mPolicy.start(dragSession, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( mPolicy.getTargets(mInsets), TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM); @@ -340,7 +339,7 @@ public class DragAndDropPolicyTest extends ShellTestCase { setRunningTask(mFullscreenAppTask); DragSession dragSession = new DragSession(mActivityTaskManager, mLandscapeDisplayLayout, mActivityClipData, 0 /* dragFlags */); - dragSession.update(); + dragSession.initialize(); mPolicy.start(dragSession, mLoggerSessionId); ArrayList<Target> targets = mPolicy.getTargets(mInsets); for (Target t : targets) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java index d38fc6cb6418..75d21457b60b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -34,7 +34,6 @@ import static org.mockito.Mockito.when; import static java.lang.Integer.MAX_VALUE; -import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Configuration; @@ -183,7 +182,7 @@ public class PipControllerTest extends ShellTestCase { @Test public void instantiatePipController_registersPipTransitionCallback() { - verify(mMockPipTransitionController).registerPipTransitionCallback(any()); + verify(mMockPipTransitionController).registerPipTransitionCallback(any(), any()); } @Test @@ -235,27 +234,6 @@ public class PipControllerTest extends ShellTestCase { } @Test - public void onActivityHidden_isLastPipComponentName_clearLastPipComponent() { - final ComponentName component1 = new ComponentName(mContext, "component1"); - when(mMockPipBoundsState.getLastPipComponentName()).thenReturn(component1); - - mPipController.mPinnedTaskListener.onActivityHidden(component1); - - verify(mMockPipBoundsState).setLastPipComponentName(null); - } - - @Test - public void onActivityHidden_isNotLastPipComponentName_lastPipComponentNotCleared() { - final ComponentName component1 = new ComponentName(mContext, "component1"); - final ComponentName component2 = new ComponentName(mContext, "component2"); - when(mMockPipBoundsState.getLastPipComponentName()).thenReturn(component1); - - mPipController.mPinnedTaskListener.onActivityHidden(component2); - - verify(mMockPipBoundsState, never()).setLastPipComponentName(null); - } - - @Test public void saveReentryState_savesPipBoundsState() { final Rect bounds = new Rect(0, 0, 10, 10); when(mMockPipBoundsAlgorithm.getSnapFraction(bounds)).thenReturn(1.0f); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java index ace09a82d71c..66f8c0b9558d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java @@ -114,8 +114,8 @@ public class PipResizeGestureHandlerTest extends ShellTestCase { final PipBoundsAlgorithm pipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, pipSnapAlgorithm, pipKeepClearAlgorithm, mPipDisplayLayoutState, mSizeSpecSource); - final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mPipBoundsState, - mPipTaskOrganizer, mPhonePipMenuController, pipSnapAlgorithm, + final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mMainExecutor, + mPipBoundsState, mPipTaskOrganizer, mPhonePipMenuController, pipSnapAlgorithm, mMockPipTransitionController, mFloatingContentCoordinator, Optional.empty() /* pipPerfHintControllerOptional */); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java index 92762fa68550..6d18e3696f84 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java @@ -116,8 +116,8 @@ public class PipTouchHandlerTest extends ShellTestCase { mPipSnapAlgorithm = new PipSnapAlgorithm(); mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, mPipSnapAlgorithm, new PipKeepClearAlgorithmInterface() {}, mPipDisplayLayoutState, mSizeSpecSource); - PipMotionHelper pipMotionHelper = new PipMotionHelper(mContext, mPipBoundsState, - mPipTaskOrganizer, mPhonePipMenuController, mPipSnapAlgorithm, + PipMotionHelper pipMotionHelper = new PipMotionHelper(mContext, mMainExecutor, + mPipBoundsState, mPipTaskOrganizer, mPhonePipMenuController, mPipSnapAlgorithm, mMockPipTransitionController, mFloatingContentCoordinator, Optional.empty() /* pipPerfHintControllerOptional */); mPipTouchHandler = new PipTouchHandler(mContext, mShellInit, mPhonePipMenuController, 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 964d86e8bd35..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; @@ -1192,7 +1191,8 @@ public class ShellTransitionTests extends ShellTestCase { mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); final RecentsTransitionHandler recentsHandler = new RecentsTransitionHandler(shellInit, transitions, - mock(RecentTasksController.class), mock(HomeTransitionObserver.class)); + mock(RecentTasksController.class), mock(HomeTransitionObserver.class), + () -> mock(SurfaceControl.Transaction.class)); transitions.replaceDefaultHandlerForTest(mDefaultHandler); shellInit.init(); @@ -1614,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<>(); @@ -1739,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/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index ca1e3f173e24..4c94c2933383 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -67,6 +67,7 @@ import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopTasksController +import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.shared.DesktopModeStatus import com.android.wm.shell.sysui.KeyguardChangeListener @@ -75,6 +76,7 @@ import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener +import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener import java.util.Optional import java.util.function.Supplier import org.junit.Assert.assertEquals @@ -82,6 +84,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.anyInt @@ -518,6 +521,99 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } } + @Test + fun testOnDecorMaximizedOrRestored_togglesTaskSize() { + val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM)) + onTaskOpening(decor.mTaskInfo) + val maxOrRestoreListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java) + .let { captor -> + verify(decor).setOnMaximizeOrRestoreClickListener(captor.capture()) + return@let captor.value + } + + maxOrRestoreListener.onClick(decor.mTaskInfo.taskId, "test") + + verify(mockDesktopTasksController).toggleDesktopTaskSize(decor.mTaskInfo) + } + + @Test + fun testOnDecorMaximizedOrRestored_closesMenus() { + val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM)) + onTaskOpening(decor.mTaskInfo) + val maxOrRestoreListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java) + .let { captor -> + verify(decor).setOnMaximizeOrRestoreClickListener(captor.capture()) + return@let captor.value + } + + maxOrRestoreListener.onClick(decor.mTaskInfo.taskId, "test") + + verify(decor).closeHandleMenu() + verify(decor).closeMaximizeMenu() + } + + @Test + fun testOnDecorSnappedLeft_snapResizes() { + val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM)) + onTaskOpening(decor.mTaskInfo) + val snapLeftListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java) + .let { captor -> + verify(decor).setOnLeftSnapClickListener(captor.capture()) + return@let captor.value + } + + snapLeftListener.onClick(decor.mTaskInfo.taskId, "test") + + verify(mockDesktopTasksController).snapToHalfScreen(decor.mTaskInfo, SnapPosition.LEFT) + } + + @Test + fun testOnDecorSnappedLeft_closeMenus() { + val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM)) + onTaskOpening(decor.mTaskInfo) + val snapLeftListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java) + .let { captor -> + verify(decor).setOnLeftSnapClickListener(captor.capture()) + return@let captor.value + } + + snapLeftListener.onClick(decor.mTaskInfo.taskId, "test") + + verify(decor).closeHandleMenu() + verify(decor).closeMaximizeMenu() + } + + @Test + fun testOnDecorSnappedRight_snapResizes() { + val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM)) + onTaskOpening(decor.mTaskInfo) + val snapLeftListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java) + .let { captor -> + verify(decor).setOnRightSnapClickListener(captor.capture()) + return@let captor.value + } + + snapLeftListener.onClick(decor.mTaskInfo.taskId, "test") + + verify(mockDesktopTasksController).snapToHalfScreen(decor.mTaskInfo, SnapPosition.RIGHT) + } + + @Test + fun testOnDecorSnappedRight_closeMenus() { + val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM)) + onTaskOpening(decor.mTaskInfo) + val snapLeftListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java) + .let { captor -> + verify(decor).setOnRightSnapClickListener(captor.capture()) + return@let captor.value + } + + snapLeftListener.onClick(decor.mTaskInfo.taskId, "test") + + verify(decor).closeHandleMenu() + verify(decor).closeMaximizeMenu() + } + private fun onTaskOpening(task: RunningTaskInfo, leash: SurfaceControl = SurfaceControl()) { desktopModeWindowDecorViewModel.onTaskOpening( task, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 46c158908226..36e8a4671a46 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -24,9 +24,14 @@ import static android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.wm.shell.MockSurfaceControlHelper.createMockSurfaceControlTransaction; +import static com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.CLOSE_MAXIMIZE_MENU_DELAY_MS; import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.doReturn; @@ -38,11 +43,13 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.content.ComponentName; +import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.PointF; import android.os.Handler; import android.os.SystemProperties; import android.platform.test.annotations.DisableFlags; @@ -62,6 +69,7 @@ import android.view.View; import android.view.WindowManager; import android.window.WindowContainerTransaction; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; @@ -76,6 +84,10 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.shared.DesktopModeStatus; import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams; +import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener; + +import kotlin.Unit; +import kotlin.jvm.functions.Function1; import org.junit.After; import org.junit.Before; @@ -84,6 +96,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.quality.Strictness; @@ -112,8 +125,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @Mock private ShellTaskOrganizer mMockShellTaskOrganizer; @Mock - private Handler mMockHandler; - @Mock private Choreographer mMockChoreographer; @Mock private SyncTransactionQueue mMockSyncQueue; @@ -131,13 +142,18 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private WindowDecoration.SurfaceControlViewHostFactory mMockSurfaceControlViewHostFactory; @Mock private TypedArray mMockRoundedCornersRadiusArray; - @Mock private TestTouchEventListener mMockTouchEventListener; @Mock private DesktopModeWindowDecoration.ExclusionRegionListener mMockExclusionRegionListener; @Mock private PackageManager mMockPackageManager; + @Mock + private Handler mMockHandler; + @Captor + private ArgumentCaptor<Function1<Boolean, Unit>> mOnMaxMenuHoverChangeListener; + @Captor + private ArgumentCaptor<Runnable> mCloseMaxMenuRunnable; private final InsetsState mInsetsState = new InsetsState(); private SurfaceControl.Transaction mMockTransaction; @@ -459,6 +475,92 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { verify(mMockHandler).removeCallbacks(runnableArgument.getValue()); } + @Test + public void createMaximizeMenu_showsMenu() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final MaximizeMenu menu = mock(MaximizeMenu.class); + final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, + new FakeMaximizeMenuFactory(menu)); + assertFalse(decoration.isMaximizeMenuActive()); + + createMaximizeMenu(decoration, menu); + + assertTrue(decoration.isMaximizeMenuActive()); + } + + @Test + public void maximizeMenu_unHoversMenu_schedulesCloseMenu() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final MaximizeMenu menu = mock(MaximizeMenu.class); + final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, + new FakeMaximizeMenuFactory(menu)); + decoration.setAppHeaderMaximizeButtonHovered(false); + createMaximizeMenu(decoration, menu); + + mOnMaxMenuHoverChangeListener.getValue().invoke(false); + + verify(mMockHandler) + .postDelayed(mCloseMaxMenuRunnable.capture(), eq(CLOSE_MAXIMIZE_MENU_DELAY_MS)); + + mCloseMaxMenuRunnable.getValue().run(); + verify(menu).close(); + assertFalse(decoration.isMaximizeMenuActive()); + } + + @Test + public void maximizeMenu_unHoversButton_schedulesCloseMenu() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final MaximizeMenu menu = mock(MaximizeMenu.class); + final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, + new FakeMaximizeMenuFactory(menu)); + decoration.setAppHeaderMaximizeButtonHovered(true); + createMaximizeMenu(decoration, menu); + + decoration.setAppHeaderMaximizeButtonHovered(false); + + verify(mMockHandler) + .postDelayed(mCloseMaxMenuRunnable.capture(), eq(CLOSE_MAXIMIZE_MENU_DELAY_MS)); + + mCloseMaxMenuRunnable.getValue().run(); + verify(menu).close(); + assertFalse(decoration.isMaximizeMenuActive()); + } + + @Test + public void maximizeMenu_hoversMenu_cancelsCloseMenu() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final MaximizeMenu menu = mock(MaximizeMenu.class); + final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, + new FakeMaximizeMenuFactory(menu)); + createMaximizeMenu(decoration, menu); + + mOnMaxMenuHoverChangeListener.getValue().invoke(true); + + verify(mMockHandler).removeCallbacks(any()); + } + + @Test + public void maximizeMenu_hoversButton_cancelsCloseMenu() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final MaximizeMenu menu = mock(MaximizeMenu.class); + final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, + new FakeMaximizeMenuFactory(menu)); + createMaximizeMenu(decoration, menu); + + decoration.setAppHeaderMaximizeButtonHovered(true); + + verify(mMockHandler).removeCallbacks(any()); + } + + private void createMaximizeMenu(DesktopModeWindowDecoration decoration, MaximizeMenu menu) { + final OnTaskActionClickListener l = (taskId, tag) -> {}; + decoration.setOnMaximizeOrRestoreClickListener(l); + decoration.setOnLeftSnapClickListener(l); + decoration.setOnRightSnapClickListener(l); + decoration.createMaximizeMenu(); + verify(menu).show(any(), any(), any(), mOnMaxMenuHoverChangeListener.capture()); + } + private void fillRoundedCornersResources(int fillValue) { when(mMockRoundedCornersRadiusArray.getDimensionPixelSize(anyInt(), anyInt())) .thenReturn(fillValue); @@ -479,12 +581,19 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private DesktopModeWindowDecoration createWindowDecoration( ActivityManager.RunningTaskInfo taskInfo) { - DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext, + return createWindowDecoration(taskInfo, new FakeMaximizeMenuFactory()); + } + + private DesktopModeWindowDecoration createWindowDecoration( + ActivityManager.RunningTaskInfo taskInfo, + MaximizeMenuFactory maximizeMenuFactory) { + final DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, mMockHandler, mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer, SurfaceControl.Builder::new, mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new, - mMockSurfaceControlViewHostFactory); + mMockSurfaceControlViewHostFactory, + maximizeMenuFactory); windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener, mMockTouchEventListener, mMockTouchEventListener); windowDecor.setExclusionRegionListener(mMockExclusionRegionListener); @@ -541,4 +650,27 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { return false; } } + + private static final class FakeMaximizeMenuFactory implements MaximizeMenuFactory { + private final MaximizeMenu mMaximizeMenu; + + FakeMaximizeMenuFactory() { + this(mock(MaximizeMenu.class)); + } + + FakeMaximizeMenuFactory(MaximizeMenu menu) { + mMaximizeMenu = menu; + } + + @NonNull + @Override + public MaximizeMenu create(@NonNull SyncTransactionQueue syncQueue, + @NonNull RootTaskDisplayAreaOrganizer rootTdaOrganizer, + @NonNull DisplayController displayController, + @NonNull ActivityManager.RunningTaskInfo taskInfo, + @NonNull Context decorWindowContext, @NonNull PointF menuPosition, + @NonNull Supplier<SurfaceControl.Transaction> transactionSupplier) { + return mMaximizeMenu; + } + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt index f750e6b9a6fe..86aded76c0f3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt @@ -21,7 +21,9 @@ import android.content.res.Resources import android.graphics.PointF import android.graphics.Rect import android.os.IBinder +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display import android.window.WindowContainerToken @@ -36,6 +38,7 @@ import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertTrue import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock @@ -53,21 +56,32 @@ import org.mockito.MockitoAnnotations class DragPositioningCallbackUtilityTest { @Mock private lateinit var mockWindowDecoration: WindowDecoration<*> + @Mock private lateinit var taskToken: WindowContainerToken + @Mock private lateinit var taskBinder: IBinder + @Mock private lateinit var mockDisplayController: DisplayController + @Mock private lateinit var mockDisplayLayout: DisplayLayout + @Mock private lateinit var mockDisplay: Display + @Mock private lateinit var mockContext: Context + @Mock private lateinit var mockResources: Resources + @JvmField + @Rule + val setFlagsRule = SetFlagsRule() + @Before fun setup() { MockitoAnnotations.initMocks(this) @@ -323,6 +337,49 @@ class DragPositioningCallbackUtilityTest { assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom - 50) } + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS) + fun testChangeBounds_windowSizeExceedsStableBounds_shouldBeAllowedToChangeBounds() { + val startingPoint = + PointF(OFF_CENTER_STARTING_BOUNDS.right.toFloat(), + OFF_CENTER_STARTING_BOUNDS.bottom.toFloat()) + val repositionTaskBounds = Rect(OFF_CENTER_STARTING_BOUNDS) + // Increase height and width by STABLE_BOUNDS. Subtract by 5px so that it doesn't reach + // the disallowed drag area. + val offset = 5 + val newX = STABLE_BOUNDS.right.toFloat() - offset + val newY = STABLE_BOUNDS.bottom.toFloat() - offset + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, OFF_CENTER_STARTING_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration) + assertThat(repositionTaskBounds.width()).isGreaterThan(STABLE_BOUNDS.right) + assertThat(repositionTaskBounds.height()).isGreaterThan(STABLE_BOUNDS.bottom) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS) + fun testChangeBoundsInDesktopMode_windowSizeExceedsStableBounds_shouldBeLimitedToDisplaySize() { + whenever(DesktopModeStatus.canEnterDesktopMode(mockContext)).thenReturn(true) + val startingPoint = + PointF(OFF_CENTER_STARTING_BOUNDS.right.toFloat(), + OFF_CENTER_STARTING_BOUNDS.bottom.toFloat()) + val repositionTaskBounds = Rect(OFF_CENTER_STARTING_BOUNDS) + // Increase height and width by STABLE_BOUNDS. Subtract by 5px so that it doesn't reach + // the disallowed drag area. + val offset = 5 + val newX = STABLE_BOUNDS.right.toFloat() - offset + val newY = STABLE_BOUNDS.bottom.toFloat() - offset + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, OFF_CENTER_STARTING_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration) + assertThat(repositionTaskBounds.width()).isLessThan(STABLE_BOUNDS.right) + assertThat(repositionTaskBounds.height()).isLessThan(STABLE_BOUNDS.bottom) + } + private fun initializeTaskInfo(taskMinWidth: Int = MIN_WIDTH, taskMinHeight: Int = MIN_HEIGHT) { mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { taskId = TASK_ID @@ -347,6 +404,7 @@ class DragPositioningCallbackUtilityTest { private const val NAVBAR_HEIGHT = 50 private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600) private val STARTING_BOUNDS = Rect(0, 0, 100, 100) + private val OFF_CENTER_STARTING_BOUNDS = Rect(-100, -100, 10, 10) private val DISALLOWED_RESIZE_AREA = Rect( DISPLAY_BOUNDS.left, DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt index 48ac1e5717aa..901ca90b573e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt @@ -17,6 +17,8 @@ package com.android.wm.shell.windowdecor import android.app.ActivityManager import android.app.WindowConfiguration +import android.content.Context +import android.content.res.Resources import android.graphics.Point import android.graphics.Rect import android.os.IBinder @@ -98,6 +100,10 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { private lateinit var mockFinishCallback: TransitionFinishCallback @Mock private lateinit var mockTransitions: Transitions + @Mock + private lateinit var mockContext: Context + @Mock + private lateinit var mockResources: Resources private lateinit var taskPositioner: VeiledResizeTaskPositioner @@ -105,6 +111,9 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) + mockDesktopWindowDecoration.mDisplay = mockDisplay + mockDesktopWindowDecoration.mDecorWindowContext = mockContext + whenever(mockContext.getResources()).thenReturn(mockResources) whenever(taskToken.asBinder()).thenReturn(taskBinder) whenever(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout) whenever(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI) 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); diff --git a/libs/nativehelper_jvm/Android.bp b/libs/nativehelper_jvm/Android.bp new file mode 100644 index 000000000000..b5b70283551a --- /dev/null +++ b/libs/nativehelper_jvm/Android.bp @@ -0,0 +1,19 @@ +package { + default_applicable_licenses: ["frameworks_base_license"], +} + +cc_library_host_static { + name: "libnativehelper_jvm", + srcs: [ + "JNIPlatformHelp.c", + "JniConstants.c", + "file_descriptor_jni.c", + ], + whole_static_libs: ["libnativehelper_any_vm"], + export_static_lib_headers: ["libnativehelper_any_vm"], + target: { + windows: { + enabled: true, + }, + }, +} diff --git a/libs/nativehelper_jvm/JNIPlatformHelp.c b/libs/nativehelper_jvm/JNIPlatformHelp.c new file mode 100644 index 000000000000..9df31a8caa7f --- /dev/null +++ b/libs/nativehelper_jvm/JNIPlatformHelp.c @@ -0,0 +1,104 @@ +/* + * 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 <nativehelper/JNIPlatformHelp.h> + +#include <stddef.h> + +#include "JniConstants.h" + +static int GetBufferPosition(JNIEnv* env, jobject nioBuffer) { + return(*env)->GetIntField(env, nioBuffer, JniConstants_NioBuffer_position(env)); +} + +static int GetBufferLimit(JNIEnv* env, jobject nioBuffer) { + return(*env)->GetIntField(env, nioBuffer, JniConstants_NioBuffer_limit(env)); +} + +static int GetBufferElementSizeShift(JNIEnv* env, jobject nioBuffer) { + jclass byteBufferClass = JniConstants_NioByteBufferClass(env); + jclass shortBufferClass = JniConstants_NioShortBufferClass(env); + jclass charBufferClass = JniConstants_NioCharBufferClass(env); + jclass intBufferClass = JniConstants_NioIntBufferClass(env); + jclass floatBufferClass = JniConstants_NioFloatBufferClass(env); + jclass longBufferClass = JniConstants_NioLongBufferClass(env); + jclass doubleBufferClass = JniConstants_NioDoubleBufferClass(env); + + // Check the type of the Buffer + if ((*env)->IsInstanceOf(env, nioBuffer, byteBufferClass)) { + return 0; + } else if ((*env)->IsInstanceOf(env, nioBuffer, shortBufferClass) || + (*env)->IsInstanceOf(env, nioBuffer, charBufferClass)) { + return 1; + } else if ((*env)->IsInstanceOf(env, nioBuffer, intBufferClass) || + (*env)->IsInstanceOf(env, nioBuffer, floatBufferClass)) { + return 2; + } else if ((*env)->IsInstanceOf(env, nioBuffer, longBufferClass) || + (*env)->IsInstanceOf(env, nioBuffer, doubleBufferClass)) { + return 3; + } + return 0; +} + +jarray jniGetNioBufferBaseArray(JNIEnv* env, jobject nioBuffer) { + jmethodID hasArrayMethod = JniConstants_NioBuffer_hasArray(env); + jboolean hasArray = (*env)->CallBooleanMethod(env, nioBuffer, hasArrayMethod); + if (hasArray) { + jmethodID arrayMethod = JniConstants_NioBuffer_array(env); + return (*env)->CallObjectMethod(env, nioBuffer, arrayMethod); + } else { + return NULL; + } +} + +int jniGetNioBufferBaseArrayOffset(JNIEnv* env, jobject nioBuffer) { + jmethodID hasArrayMethod = JniConstants_NioBuffer_hasArray(env); + jboolean hasArray = (*env)->CallBooleanMethod(env, nioBuffer, hasArrayMethod); + if (hasArray) { + jmethodID arrayOffsetMethod = JniConstants_NioBuffer_arrayOffset(env); + jint arrayOffset = (*env)->CallIntMethod(env, nioBuffer, arrayOffsetMethod); + const int position = GetBufferPosition(env, nioBuffer); + jint elementSizeShift = GetBufferElementSizeShift(env, nioBuffer); + return (arrayOffset + position) << elementSizeShift; + } else { + return 0; + } +} + +jlong jniGetNioBufferPointer(JNIEnv* env, jobject nioBuffer) { + // in Java 11, the address field of a HeapByteBuffer contains a non-zero value despite + // HeapByteBuffer being a non-direct buffer. In that case, this should still return 0. + jmethodID isDirectMethod = JniConstants_NioBuffer_isDirect(env); + jboolean isDirect = (*env)->CallBooleanMethod(env, nioBuffer, isDirectMethod); + if (isDirect == JNI_FALSE) { + return 0L; + } + jlong baseAddress = (*env)->GetLongField(env, nioBuffer, JniConstants_NioBuffer_address(env)); + if (baseAddress != 0) { + const int position = GetBufferPosition(env, nioBuffer); + const int shift = GetBufferElementSizeShift(env, nioBuffer); + baseAddress += position << shift; + } + return baseAddress; +} + +jlong jniGetNioBufferFields(JNIEnv* env, jobject nioBuffer, + jint* position, jint* limit, jint* elementSizeShift) { + *position = GetBufferPosition(env, nioBuffer); + *limit = GetBufferLimit(env, nioBuffer); + *elementSizeShift = GetBufferElementSizeShift(env, nioBuffer); + return (*env)->GetLongField(env, nioBuffer, JniConstants_NioBuffer_address(env)); +} diff --git a/libs/nativehelper_jvm/JniConstants.c b/libs/nativehelper_jvm/JniConstants.c new file mode 100644 index 000000000000..ca58f61070ba --- /dev/null +++ b/libs/nativehelper_jvm/JniConstants.c @@ -0,0 +1,199 @@ +/* + * 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 "JniConstants.h" + +#include <pthread.h> +#include <stdbool.h> +#include <stddef.h> +#include <string.h> + +#define LOG_TAG "JniConstants" +#include <log/log.h> + +// jclass constants list: +// <class, signature, androidOnly> +#define JCLASS_CONSTANTS_LIST(V) \ + V(FileDescriptor, "java/io/FileDescriptor", false) \ + V(NioBuffer, "java/nio/Buffer", false) \ + V(NioByteBuffer, "java/nio/ByteBuffer", false) \ + V(NioShortBuffer, "java/nio/ShortBuffer", false) \ + V(NioCharBuffer, "java/nio/CharBuffer", false) \ + V(NioIntBuffer, "java/nio/IntBuffer", false) \ + V(NioFloatBuffer, "java/nio/FloatBuffer", false) \ + V(NioLongBuffer, "java/nio/LongBuffer", false) \ + V(NioDoubleBuffer, "java/nio/DoubleBuffer", false) + +// jmethodID's of public methods constants list: +// <Class, method, method-string, signature, is_static> +#define JMETHODID_CONSTANTS_LIST(V) \ + V(FileDescriptor, init, "<init>", "()V", false) \ + V(NioBuffer, array, "array", "()Ljava/lang/Object;", false) \ + V(NioBuffer, hasArray, "hasArray", "()Z", false) \ + V(NioBuffer, isDirect, "isDirect", "()Z", false) \ + V(NioBuffer, arrayOffset, "arrayOffset", "()I", false) + +// jfieldID constants list: +// <Class, field, signature, is_static> +#define JFIELDID_CONSTANTS_LIST(V) \ + V(FileDescriptor, fd, "I", false) \ + V(NioBuffer, address, "J", false) \ + V(NioBuffer, limit, "I", false) \ + V(NioBuffer, position, "I", false) + +#define CLASS_NAME(cls) g_ ## cls +#define METHOD_NAME(cls, method) g_ ## cls ## _ ## method +#define FIELD_NAME(cls, field) g_ ## cls ## _ ## field + +// +// Declare storage for cached classes, methods and fields. +// + +#define JCLASS_DECLARE_STORAGE(cls, ...) \ + static jclass CLASS_NAME(cls) = NULL; +JCLASS_CONSTANTS_LIST(JCLASS_DECLARE_STORAGE) +#undef JCLASS_DECLARE_STORAGE + +#define JMETHODID_DECLARE_STORAGE(cls, method, ...) \ + static jmethodID METHOD_NAME(cls, method) = NULL; +JMETHODID_CONSTANTS_LIST(JMETHODID_DECLARE_STORAGE) +#undef JMETHODID_DECLARE_STORAGE + +#define JFIELDID_DECLARE_STORAGE(cls, field, ...) \ + static jfieldID FIELD_NAME(cls, field) = NULL; +JFIELDID_CONSTANTS_LIST(JFIELDID_DECLARE_STORAGE) +#undef JFIELDID_DECLARE_STORAGE + +// +// Helper methods +// + +static jclass FindClass(JNIEnv* env, const char* signature, bool androidOnly) { + jclass cls = (*env)->FindClass(env, signature); + if (cls == NULL) { + LOG_ALWAYS_FATAL_IF(!androidOnly, "Class not found: %s", signature); + return NULL; + } + return (*env)->NewGlobalRef(env, cls); +} + +static jmethodID FindMethod(JNIEnv* env, jclass cls, + const char* name, const char* signature, bool isStatic) { + jmethodID method; + if (isStatic) { + method = (*env)->GetStaticMethodID(env, cls, name, signature); + } else { + method = (*env)->GetMethodID(env, cls, name, signature); + } + LOG_ALWAYS_FATAL_IF(method == NULL, "Method not found: %s:%s", name, signature); + return method; +} + +static jfieldID FindField(JNIEnv* env, jclass cls, + const char* name, const char* signature, bool isStatic) { + jfieldID field; + if (isStatic) { + field = (*env)->GetStaticFieldID(env, cls, name, signature); + } else { + field = (*env)->GetFieldID(env, cls, name, signature); + } + LOG_ALWAYS_FATAL_IF(field == NULL, "Field not found: %s:%s", name, signature); + return field; +} + +static pthread_once_t g_initialized = PTHREAD_ONCE_INIT; +static JNIEnv* g_init_env; + +static void InitializeConstants() { + // Initialize cached classes. +#define JCLASS_INITIALIZE(cls, signature, androidOnly) \ + CLASS_NAME(cls) = FindClass(g_init_env, signature, androidOnly); + JCLASS_CONSTANTS_LIST(JCLASS_INITIALIZE) +#undef JCLASS_INITIALIZE + + // Initialize cached methods. +#define JMETHODID_INITIALIZE(cls, method, name, signature, isStatic) \ + METHOD_NAME(cls, method) = \ + FindMethod(g_init_env, CLASS_NAME(cls), name, signature, isStatic); + JMETHODID_CONSTANTS_LIST(JMETHODID_INITIALIZE) +#undef JMETHODID_INITIALIZE + + // Initialize cached fields. +#define JFIELDID_INITIALIZE(cls, field, signature, isStatic) \ + FIELD_NAME(cls, field) = \ + FindField(g_init_env, CLASS_NAME(cls), #field, signature, isStatic); + JFIELDID_CONSTANTS_LIST(JFIELDID_INITIALIZE) +#undef JFIELDID_INITIALIZE +} + +void EnsureInitialized(JNIEnv* env) { + // This method has to be called in every cache accesses because library can be built + // 2 different ways and existing usage for compat version doesn't have a good hook for + // initialization and is widely used. + g_init_env = env; + pthread_once(&g_initialized, InitializeConstants); +} + +// API exported by libnativehelper_api.h. + +void jniUninitializeConstants() { + // Uninitialize cached classes, methods and fields. + // + // NB we assume the runtime is stopped at this point and do not delete global + // references. +#define JCLASS_INVALIDATE(cls, ...) CLASS_NAME(cls) = NULL; + JCLASS_CONSTANTS_LIST(JCLASS_INVALIDATE); +#undef JCLASS_INVALIDATE + +#define JMETHODID_INVALIDATE(cls, method, ...) METHOD_NAME(cls, method) = NULL; + JMETHODID_CONSTANTS_LIST(JMETHODID_INVALIDATE); +#undef JMETHODID_INVALIDATE + +#define JFIELDID_INVALIDATE(cls, field, ...) FIELD_NAME(cls, field) = NULL; + JFIELDID_CONSTANTS_LIST(JFIELDID_INVALIDATE); +#undef JFIELDID_INVALIDATE + + // If jniConstantsUninitialize is called, runtime has shutdown. Reset + // state as some tests re-start the runtime. + pthread_once_t o = PTHREAD_ONCE_INIT; + memcpy(&g_initialized, &o, sizeof(o)); +} + +// +// Accessors +// + +#define JCLASS_ACCESSOR_IMPL(cls, ...) \ +jclass JniConstants_ ## cls ## Class(JNIEnv* env) { \ + EnsureInitialized(env); \ + return CLASS_NAME(cls); \ +} +JCLASS_CONSTANTS_LIST(JCLASS_ACCESSOR_IMPL) +#undef JCLASS_ACCESSOR_IMPL + +#define JMETHODID_ACCESSOR_IMPL(cls, method, ...) \ +jmethodID JniConstants_ ## cls ## _ ## method(JNIEnv* env) { \ + EnsureInitialized(env); \ + return METHOD_NAME(cls, method); \ +} +JMETHODID_CONSTANTS_LIST(JMETHODID_ACCESSOR_IMPL) + +#define JFIELDID_ACCESSOR_IMPL(cls, field, ...) \ +jfieldID JniConstants_ ## cls ## _ ## field(JNIEnv* env) { \ + EnsureInitialized(env); \ + return FIELD_NAME(cls, field); \ +} +JFIELDID_CONSTANTS_LIST(JFIELDID_ACCESSOR_IMPL) diff --git a/libs/nativehelper_jvm/JniConstants.h b/libs/nativehelper_jvm/JniConstants.h new file mode 100644 index 000000000000..e7a266d72509 --- /dev/null +++ b/libs/nativehelper_jvm/JniConstants.h @@ -0,0 +1,63 @@ +/* + * Copyright 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 <sys/cdefs.h> + +#include <jni.h> + +__BEGIN_DECLS + +// +// Classes in constants cache. +// +// NB The implementations of these methods are generated by the JCLASS_ACCESSOR_IMPL macro in +// JniConstants.c. +// +jclass JniConstants_FileDescriptorClass(JNIEnv* env); +jclass JniConstants_NioByteBufferClass(JNIEnv* env); +jclass JniConstants_NioShortBufferClass(JNIEnv* env); +jclass JniConstants_NioCharBufferClass(JNIEnv* env); +jclass JniConstants_NioIntBufferClass(JNIEnv* env); +jclass JniConstants_NioFloatBufferClass(JNIEnv* env); +jclass JniConstants_NioLongBufferClass(JNIEnv* env); +jclass JniConstants_NioDoubleBufferClass(JNIEnv* env); + +// +// Methods in the constants cache. +// +// NB The implementations of these methods are generated by the JMETHODID_ACCESSOR_IMPL macro in +// JniConstants.c. +// +jmethodID JniConstants_FileDescriptor_init(JNIEnv* env); +jmethodID JniConstants_NioBuffer_array(JNIEnv* env); +jmethodID JniConstants_NioBuffer_arrayOffset(JNIEnv* env); +jmethodID JniConstants_NioBuffer_hasArray(JNIEnv* env); +jmethodID JniConstants_NioBuffer_isDirect(JNIEnv* env); + +// +// Fields in the constants cache. +// +// NB The implementations of these methods are generated by the JFIELDID_ACCESSOR_IMPL macro in +// JniConstants.c. +// +jfieldID JniConstants_FileDescriptor_fd(JNIEnv* env); +jfieldID JniConstants_NioBuffer_address(JNIEnv* env); +jfieldID JniConstants_NioBuffer_limit(JNIEnv* env); +jfieldID JniConstants_NioBuffer_position(JNIEnv* env); + +__END_DECLS diff --git a/libs/nativehelper_jvm/OWNERS b/libs/nativehelper_jvm/OWNERS new file mode 100644 index 000000000000..5d55f6e4319b --- /dev/null +++ b/libs/nativehelper_jvm/OWNERS @@ -0,0 +1,7 @@ +# Bug component: 326772 + +include /libs/hwui/OWNERS +include platform/libnativehelper:/OWNERS + +diegoperez@google.com +jgaillard@google.com diff --git a/libs/nativehelper_jvm/README b/libs/nativehelper_jvm/README new file mode 100644 index 000000000000..755c42261f43 --- /dev/null +++ b/libs/nativehelper_jvm/README @@ -0,0 +1,2 @@ +libnativehelper_jvm is a JVM-compatible version of libnativehelper. +It should be used instead of libnativehelper whenever a host library is meant to run on a JVM.
\ No newline at end of file diff --git a/libs/nativehelper_jvm/file_descriptor_jni.c b/libs/nativehelper_jvm/file_descriptor_jni.c new file mode 100644 index 000000000000..36880cd586ca --- /dev/null +++ b/libs/nativehelper_jvm/file_descriptor_jni.c @@ -0,0 +1,47 @@ +/* + * 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 <android/file_descriptor_jni.h> + +#include <stddef.h> + +#define LOG_TAG "file_descriptor_jni" +#include <log/log.h> + +#include "JniConstants.h" + +static void EnsureArgumentIsFileDescriptor(JNIEnv* env, jobject instance) { + LOG_ALWAYS_FATAL_IF(instance == NULL, "FileDescriptor is NULL"); + jclass jifd = JniConstants_FileDescriptorClass(env); + LOG_ALWAYS_FATAL_IF(!(*env)->IsInstanceOf(env, instance, jifd), + "Argument is not a FileDescriptor"); +} + +JNIEXPORT _Nullable jobject AFileDescriptor_create(JNIEnv* env) { + return (*env)->NewObject(env, + JniConstants_FileDescriptorClass(env), + JniConstants_FileDescriptor_init(env)); +} + +JNIEXPORT int AFileDescriptor_getFd(JNIEnv* env, jobject fileDescriptor) { + EnsureArgumentIsFileDescriptor(env, fileDescriptor); + return (*env)->GetIntField(env, fileDescriptor, JniConstants_FileDescriptor_fd(env)); +} + +JNIEXPORT void AFileDescriptor_setFd(JNIEnv* env, jobject fileDescriptor, int fd) { + EnsureArgumentIsFileDescriptor(env, fileDescriptor); + (*env)->SetIntField(env, fileDescriptor, JniConstants_FileDescriptor_fd(env), fd); +} |